• 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

98.43
/freqtrade/optimize/optimize_reports/bt_output.py
1
import logging
1✔
2
from typing import Any, Dict, List
1✔
3

4
from tabulate import tabulate
1✔
5

6
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
1✔
7
from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats
1✔
8
from freqtrade.types import BacktestResultType
1✔
9
from freqtrade.util import decimals_per_coin, fmt_coin
1✔
10

11

12
logger = logging.getLogger(__name__)
1✔
13

14

15
def _get_line_floatfmt(stake_currency: str) -> List[str]:
1✔
16
    """
17
    Generate floatformat (goes in line with _generate_result_line())
18
    """
19
    return ['s', 'd', '.2f', f'.{decimals_per_coin(stake_currency)}f',
1✔
20
            '.2f', 'd', 's', 's']
21

22

23
def _get_line_header(first_column: str, stake_currency: str,
1✔
24
                     direction: str = 'Entries') -> List[str]:
25
    """
26
    Generate header lines (goes in line with _generate_result_line())
27
    """
28
    return [first_column, direction, 'Avg Profit %',
1✔
29
            f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
30
            'Win  Draw  Loss  Win%']
31

32

33
def generate_wins_draws_losses(wins, draws, losses):
1✔
34
    if wins > 0 and losses == 0:
1✔
35
        wl_ratio = '100'
1✔
36
    elif wins == 0:
1✔
37
        wl_ratio = '0'
1✔
38
    else:
39
        wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100'
1✔
40
    return f'{wins:>4}  {draws:>4}  {losses:>4}  {wl_ratio:>4}'
1✔
41

42

43
def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
1✔
44
    """
45
    Generates and returns a text table for the given backtest data and the results dataframe
46
    :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
47
    :param stake_currency: stake-currency - used to correctly name headers
48
    :return: pretty printed table with tabulate as string
49
    """
50

51
    headers = _get_line_header('Pair', stake_currency)
1✔
52
    floatfmt = _get_line_floatfmt(stake_currency)
1✔
53
    output = [[
1✔
54
        t['key'], t['trades'], t['profit_mean_pct'], t['profit_total_abs'],
55
        t['profit_total_pct'], t['duration_avg'],
56
        generate_wins_draws_losses(t['wins'], t['draws'], t['losses'])
57
    ] for t in pair_results]
58
    # Ignore type as floatfmt does allow tuples but mypy does not know that
59
    return tabulate(output, headers=headers,
1✔
60
                    floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
61

62

63
def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str:
1✔
64
    """
65
    Generates and returns a text table for the given backtest data and the results dataframe
66
    :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
67
    :param stake_currency: stake-currency - used to correctly name headers
68
    :return: pretty printed table with tabulate as string
69
    """
70
    fallback: str = ''
1✔
71
    if (tag_type == "enter_tag"):
1✔
72
        headers = _get_line_header("TAG", stake_currency)
1✔
73
    else:
74
        headers = _get_line_header("Exit Reason", stake_currency, 'Exits')
1✔
75
        fallback = 'exit_reason'
1✔
76

77
    floatfmt = _get_line_floatfmt(stake_currency)
1✔
78
    output = [
1✔
79
        [
80
            t['key'] if t.get('key') is not None and len(
81
                str(t['key'])) > 0 else t.get(fallback, "OTHER"),
82
            t['trades'],
83
            t['profit_mean_pct'],
84
            t['profit_total_abs'],
85
            t['profit_total_pct'],
86
            t.get('duration_avg'),
87
            generate_wins_draws_losses(
88
                t['wins'],
89
                t['draws'],
90
                t['losses'])] for t in tag_results]
91
    # Ignore type as floatfmt does allow tuples but mypy does not know that
92
    return tabulate(output, headers=headers,
1✔
93
                    floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
94

95

96
def text_table_periodic_breakdown(days_breakdown_stats: List[Dict[str, Any]],
1✔
97
                                  stake_currency: str, period: str) -> str:
98
    """
99
    Generate small table with Backtest results by days
100
    :param days_breakdown_stats: Days breakdown metrics
101
    :param stake_currency: Stakecurrency used
102
    :return: pretty printed table with tabulate as string
103
    """
104
    headers = [
1✔
105
        period.capitalize(),
106
        f'Tot Profit {stake_currency}',
107
        'Wins',
108
        'Draws',
109
        'Losses',
110
    ]
111
    output = [[
1✔
112
        d['date'], fmt_coin(d['profit_abs'], stake_currency, False),
113
        d['wins'], d['draws'], d['loses'],
114
    ] for d in days_breakdown_stats]
115
    return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
1✔
116

117

118
def text_table_strategy(strategy_results, stake_currency: str) -> str:
1✔
119
    """
120
    Generate summary table per strategy
121
    :param strategy_results: Dict of <Strategyname: DataFrame> containing results for all strategies
122
    :param stake_currency: stake-currency - used to correctly name headers
123
    :return: pretty printed table with tabulate as string
124
    """
125
    floatfmt = _get_line_floatfmt(stake_currency)
1✔
126
    headers = _get_line_header('Strategy', stake_currency)
1✔
127
    # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless
128
    # therefore we slip this column in only for strategy summary here.
129
    headers.append('Drawdown')
1✔
130

131
    # Align drawdown string on the center two space separator.
132
    if 'max_drawdown_account' in strategy_results[0]:
1✔
133
        drawdown = [f'{t["max_drawdown_account"] * 100:.2f}' for t in strategy_results]
1✔
134
    else:
135
        # Support for prior backtest results
136
        drawdown = [f'{t["max_drawdown_per"]:.2f}' for t in strategy_results]
×
137

138
    dd_pad_abs = max([len(t['max_drawdown_abs']) for t in strategy_results])
1✔
139
    dd_pad_per = max([len(dd) for dd in drawdown])
1✔
140
    drawdown = [f'{t["max_drawdown_abs"]:>{dd_pad_abs}} {stake_currency}  {dd:>{dd_pad_per}}%'
1✔
141
                for t, dd in zip(strategy_results, drawdown)]
142

143
    output = [[
1✔
144
        t['key'], t['trades'], t['profit_mean_pct'], t['profit_total_abs'],
145
        t['profit_total_pct'], t['duration_avg'],
146
        generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown]
147
        for t, drawdown in zip(strategy_results, drawdown)]
148
    # Ignore type as floatfmt does allow tuples but mypy does not know that
149
    return tabulate(output, headers=headers,
1✔
150
                    floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
151

152

153
def text_table_add_metrics(strat_results: Dict) -> str:
1✔
154
    if len(strat_results['trades']) > 0:
1✔
155
        best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio'])
1✔
156
        worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio'])
1✔
157

158
        short_metrics = [
1✔
159
            ('', ''),  # Empty line to improve readability
160
            ('Long / Short',
161
             f"{strat_results.get('trade_count_long', 'total_trades')} / "
162
             f"{strat_results.get('trade_count_short', 0)}"),
163
            ('Total profit Long %', f"{strat_results['profit_total_long']:.2%}"),
164
            ('Total profit Short %', f"{strat_results['profit_total_short']:.2%}"),
165
            ('Absolute profit Long', fmt_coin(strat_results['profit_total_long_abs'],
166
                                              strat_results['stake_currency'])),
167
            ('Absolute profit Short', fmt_coin(strat_results['profit_total_short_abs'],
168
                                               strat_results['stake_currency'])),
169
        ] if strat_results.get('trade_count_short', 0) > 0 else []
170

171
        drawdown_metrics = []
1✔
172
        if 'max_relative_drawdown' in strat_results:
1✔
173
            # Compatibility to show old hyperopt results
174
            drawdown_metrics.append(
1✔
175
                ('Max % of account underwater', f"{strat_results['max_relative_drawdown']:.2%}")
176
            )
177
        drawdown_metrics.extend([
1✔
178
            ('Absolute Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}")
179
            if 'max_drawdown_account' in strat_results else (
180
                'Drawdown', f"{strat_results['max_drawdown']:.2%}"),
181
            ('Absolute Drawdown', fmt_coin(strat_results['max_drawdown_abs'],
182
                                           strat_results['stake_currency'])),
183
            ('Drawdown high', fmt_coin(strat_results['max_drawdown_high'],
184
                                       strat_results['stake_currency'])),
185
            ('Drawdown low', fmt_coin(strat_results['max_drawdown_low'],
186
                                      strat_results['stake_currency'])),
187
            ('Drawdown Start', strat_results['drawdown_start']),
188
            ('Drawdown End', strat_results['drawdown_end']),
189
        ])
190

191
        entry_adjustment_metrics = [
1✔
192
            ('Canceled Trade Entries', strat_results.get('canceled_trade_entries', 'N/A')),
193
            ('Canceled Entry Orders', strat_results.get('canceled_entry_orders', 'N/A')),
194
            ('Replaced Entry Orders', strat_results.get('replaced_entry_orders', 'N/A')),
195
        ] if strat_results.get('canceled_entry_orders', 0) > 0 else []
196

197
        # Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
198
        # command stores these results and newer version of freqtrade must be able to handle old
199
        # results with missing new fields.
200
        metrics = [
1✔
201
            ('Backtesting from', strat_results['backtest_start']),
202
            ('Backtesting to', strat_results['backtest_end']),
203
            ('Max open trades', strat_results['max_open_trades']),
204
            ('', ''),  # Empty line to improve readability
205
            ('Total/Daily Avg Trades',
206
                f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"),
207

208
            ('Starting balance', fmt_coin(strat_results['starting_balance'],
209
                                          strat_results['stake_currency'])),
210
            ('Final balance', fmt_coin(strat_results['final_balance'],
211
                                       strat_results['stake_currency'])),
212
            ('Absolute profit ', fmt_coin(strat_results['profit_total_abs'],
213
                                          strat_results['stake_currency'])),
214
            ('Total profit %', f"{strat_results['profit_total']:.2%}"),
215
            ('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
216
            ('Sortino', f"{strat_results['sortino']:.2f}" if 'sortino' in strat_results else 'N/A'),
217
            ('Sharpe', f"{strat_results['sharpe']:.2f}" if 'sharpe' in strat_results else 'N/A'),
218
            ('Calmar', f"{strat_results['calmar']:.2f}" if 'calmar' in strat_results else 'N/A'),
219
            ('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
220
                              in strat_results else 'N/A'),
221
            ('Expectancy (Ratio)', (
222
                f"{strat_results['expectancy']:.2f} ({strat_results['expectancy_ratio']:.2f})" if
223
                'expectancy_ratio' in strat_results else 'N/A')),
224
            ('Trades per day', strat_results['trades_per_day']),
225
            ('Avg. daily profit %',
226
             f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
227
            ('Avg. stake amount', fmt_coin(strat_results['avg_stake_amount'],
228
                                           strat_results['stake_currency'])),
229
            ('Total trade volume', fmt_coin(strat_results['total_volume'],
230
                                            strat_results['stake_currency'])),
231
            *short_metrics,
232
            ('', ''),  # Empty line to improve readability
233
            ('Best Pair', f"{strat_results['best_pair']['key']} "
234
                          f"{strat_results['best_pair']['profit_total']:.2%}"),
235
            ('Worst Pair', f"{strat_results['worst_pair']['key']} "
236
                           f"{strat_results['worst_pair']['profit_total']:.2%}"),
237
            ('Best trade', f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"),
238
            ('Worst trade', f"{worst_trade['pair']} "
239
                            f"{worst_trade['profit_ratio']:.2%}"),
240

241
            ('Best day', fmt_coin(strat_results['backtest_best_day_abs'],
242
                                  strat_results['stake_currency'])),
243
            ('Worst day', fmt_coin(strat_results['backtest_worst_day_abs'],
244
                                   strat_results['stake_currency'])),
245
            ('Days win/draw/lose', f"{strat_results['winning_days']} / "
246
                f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
247
            ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
248
            ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
249
            ('Max Consecutive Wins / Loss',
250
             f"{strat_results['max_consecutive_wins']} / {strat_results['max_consecutive_losses']}"
251
             if 'max_consecutive_losses' in strat_results else 'N/A'),
252
            ('Rejected Entry signals', strat_results.get('rejected_signals', 'N/A')),
253
            ('Entry/Exit Timeouts',
254
             f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
255
             f"{strat_results.get('timedout_exit_orders', 'N/A')}"),
256
            *entry_adjustment_metrics,
257
            ('', ''),  # Empty line to improve readability
258

259
            ('Min balance', fmt_coin(strat_results['csum_min'], strat_results['stake_currency'])),
260
            ('Max balance', fmt_coin(strat_results['csum_max'], strat_results['stake_currency'])),
261

262
            *drawdown_metrics,
263
            ('Market change', f"{strat_results['market_change']:.2%}"),
264
        ]
265

266
        return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
1✔
267
    else:
268
        start_balance = fmt_coin(strat_results['starting_balance'], strat_results['stake_currency'])
1✔
269
        stake_amount = fmt_coin(
1✔
270
            strat_results['stake_amount'], strat_results['stake_currency']
271
        ) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
272

273
        message = ("No trades made. "
1✔
274
                   f"Your starting balance was {start_balance}, "
275
                   f"and your stake was {stake_amount}."
276
                   )
277
        return message
1✔
278

279

280
def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str,
1✔
281
                         backtest_breakdown: List[str]):
282
    """
283
    Print results for one strategy
284
    """
285
    # Print results
286
    print(f"Result for strategy {strategy}")
1✔
287
    table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency)
1✔
288
    if isinstance(table, str):
1✔
289
        print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
1✔
290
    print(table)
1✔
291

292
    table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
1✔
293
    if isinstance(table, str) and len(table) > 0:
1✔
294
        print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
1✔
295
    print(table)
1✔
296

297
    if (enter_tags := results.get('results_per_enter_tag')) is not None:
1✔
298
        table = text_table_tags("enter_tag", enter_tags, stake_currency)
1✔
299

300
        if isinstance(table, str) and len(table) > 0:
1✔
301
            print(' ENTER TAG STATS '.center(len(table.splitlines()[0]), '='))
1✔
302
        print(table)
1✔
303

304
    if (exit_reasons := results.get('exit_reason_summary')) is not None:
1✔
305
        table = text_table_tags("exit_tag", exit_reasons, stake_currency)
1✔
306

307
        if isinstance(table, str) and len(table) > 0:
1✔
308
            print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
1✔
309
        print(table)
1✔
310

311
    for period in backtest_breakdown:
1✔
312
        if period in results.get('periodic_breakdown', {}):
1✔
313
            days_breakdown_stats = results['periodic_breakdown'][period]
1✔
314
        else:
315
            days_breakdown_stats = generate_periodic_breakdown_stats(
×
316
                trade_list=results['trades'], period=period)
317
        table = text_table_periodic_breakdown(days_breakdown_stats=days_breakdown_stats,
1✔
318
                                              stake_currency=stake_currency, period=period)
319
        if isinstance(table, str) and len(table) > 0:
1✔
320
            print(f' {period.upper()} BREAKDOWN '.center(len(table.splitlines()[0]), '='))
1✔
321
        print(table)
1✔
322

323
    table = text_table_add_metrics(results)
1✔
324
    if isinstance(table, str) and len(table) > 0:
1✔
325
        print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '='))
1✔
326
    print(table)
1✔
327

328
    if isinstance(table, str) and len(table) > 0:
1✔
329
        print('=' * len(table.splitlines()[0]))
1✔
330

331
    print()
1✔
332

333

334
def show_backtest_results(config: Config, backtest_stats: BacktestResultType):
1✔
335
    stake_currency = config['stake_currency']
1✔
336

337
    for strategy, results in backtest_stats['strategy'].items():
1✔
338
        show_backtest_result(
1✔
339
            strategy, results, stake_currency,
340
            config.get('backtest_breakdown', []))
341

342
    if len(backtest_stats['strategy']) > 0:
1✔
343
        # Print Strategy summary table
344

345
        table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
1✔
346
        print(f"Backtested {results['backtest_start']} -> {results['backtest_end']} |"
1✔
347
              f" Max open trades : {results['max_open_trades']}")
348
        print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
1✔
349
        print(table)
1✔
350
        print('=' * len(table.splitlines()[0]))
1✔
351
        print('\nFor more details, please look at the detail tables above')
1✔
352

353

354
def show_sorted_pairlist(config: Config, backtest_stats: BacktestResultType):
1✔
355
    if config.get('backtest_show_pair_list', False):
1✔
356
        for strategy, results in backtest_stats['strategy'].items():
1✔
357
            print(f"Pairs for Strategy {strategy}: \n[")
1✔
358
            for result in results['results_per_pair']:
1✔
359
                if result["key"] != 'TOTAL':
1✔
360
                    print(f'"{result["key"]}",  // {result["profit_mean"]:.2%}')
1✔
361
            print("]")
1✔
362

363

364
def generate_edge_table(results: dict) -> str:
1✔
365
    floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
1✔
366
    tabular_data = []
1✔
367
    headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
1✔
368
               'Required Risk Reward', 'Expectancy', 'Total Number of Trades',
369
               'Average Duration (min)']
370

371
    for result in results.items():
1✔
372
        if result[1].nb_trades > 0:
1✔
373
            tabular_data.append([
1✔
374
                result[0],
375
                result[1].stoploss,
376
                result[1].winrate,
377
                result[1].risk_reward_ratio,
378
                result[1].required_risk_reward,
379
                result[1].expectancy,
380
                result[1].nb_trades,
381
                round(result[1].avg_trade_duration)
382
            ])
383

384
    # Ignore type as floatfmt does allow tuples but mypy does not know that
385
    return tabulate(tabular_data, headers=headers,
1✔
386
                    floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
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