• 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

92.34
/freqtrade/plot/plotting.py
1
import logging
1✔
2
from datetime import datetime, timezone
1✔
3
from pathlib import Path
1✔
4
from typing import Dict, List, Optional
1✔
5

6
import pandas as pd
1✔
7

8
from freqtrade.configuration import TimeRange
1✔
9
from freqtrade.constants import Config
1✔
10
from freqtrade.data.btanalysis import (analyze_trade_parallelism, extract_trades_of_period,
1✔
11
                                       load_trades)
12
from freqtrade.data.converter import trim_dataframe
1✔
13
from freqtrade.data.dataprovider import DataProvider
1✔
14
from freqtrade.data.history import get_timerange, load_data
1✔
15
from freqtrade.data.metrics import (calculate_max_drawdown, calculate_underwater,
1✔
16
                                    combine_dataframes_with_mean, create_cum_profit)
17
from freqtrade.enums import CandleType
1✔
18
from freqtrade.exceptions import OperationalException
1✔
19
from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_seconds
1✔
20
from freqtrade.misc import pair_to_filename
1✔
21
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
1✔
22
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
1✔
23
from freqtrade.strategy import IStrategy
1✔
24
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
1✔
25

26

27
logger = logging.getLogger(__name__)
1✔
28

29

30
try:
1✔
31
    import plotly.graph_objects as go
1✔
32
    from plotly.offline import plot
1✔
33
    from plotly.subplots import make_subplots
1✔
34
except ImportError:
×
35
    logger.exception("Module plotly not found \n Please install using `pip3 install plotly`")
×
36
    exit(1)
×
37

38

39
def init_plotscript(config, markets: List, startup_candles: int = 0):
1✔
40
    """
41
    Initialize objects needed for plotting
42
    :return: Dict with candle (OHLCV) data, trades and pairs
43
    """
44

45
    if "pairs" in config:
1✔
46
        pairs = expand_pairlist(config['pairs'], markets)
1✔
47
    else:
48
        pairs = expand_pairlist(config['exchange']['pair_whitelist'], markets)
1✔
49

50
    # Set timerange to use
51
    timerange = TimeRange.parse_timerange(config.get('timerange'))
1✔
52

53
    data = load_data(
1✔
54
        datadir=config.get('datadir'),
55
        pairs=pairs,
56
        timeframe=config['timeframe'],
57
        timerange=timerange,
58
        startup_candles=startup_candles,
59
        data_format=config['dataformat_ohlcv'],
60
        candle_type=config.get('candle_type_def', CandleType.SPOT)
61
    )
62

63
    if startup_candles and data:
1✔
64
        min_date, max_date = get_timerange(data)
1✔
65
        logger.info(f"Loading data from {min_date} to {max_date}")
1✔
66
        timerange.adjust_start_if_necessary(timeframe_to_seconds(config['timeframe']),
1✔
67
                                            startup_candles, min_date)
68

69
    no_trades = False
1✔
70
    filename = config.get("exportfilename")
1✔
71
    if config.get("no_trades", False):
1✔
72
        no_trades = True
×
73
    elif config['trade_source'] == 'file':
1✔
74
        if not filename.is_dir() and not filename.is_file():
1✔
75
            logger.warning("Backtest file is missing skipping trades.")
1✔
76
            no_trades = True
1✔
77
    try:
1✔
78
        trades = load_trades(
1✔
79
            config['trade_source'],
80
            db_url=config.get('db_url'),
81
            exportfilename=filename,
82
            no_trades=no_trades,
83
            strategy=config.get('strategy'),
84
        )
85
    except ValueError as e:
×
86
        raise OperationalException(e) from e
×
87
    if not trades.empty:
1✔
88
        trades = trim_dataframe(trades, timerange, df_date_col='open_date')
1✔
89

90
    return {"ohlcv": data,
1✔
91
            "trades": trades,
92
            "pairs": pairs,
93
            "timerange": timerange,
94
            }
95

96

97
def add_indicators(fig, row, indicators: Dict[str, Dict], data: pd.DataFrame) -> make_subplots:
1✔
98
    """
99
    Generate all the indicators selected by the user for a specific row, based on the configuration
100
    :param fig: Plot figure to append to
101
    :param row: row number for this plot
102
    :param indicators: Dict of Indicators with configuration options.
103
                       Dict key must correspond to dataframe column.
104
    :param data: candlestick DataFrame
105
    """
106
    plot_kinds = {
1✔
107
        'scatter': go.Scatter,
108
        'bar': go.Bar,
109
    }
110
    for indicator, conf in indicators.items():
1✔
111
        logger.debug(f"indicator {indicator} with config {conf}")
1✔
112
        if indicator in data:
1✔
113
            kwargs = {'x': data['date'],
1✔
114
                      'y': data[indicator].values,
115
                      'name': indicator
116
                      }
117

118
            plot_type = conf.get('type', 'scatter')
1✔
119
            color = conf.get('color')
1✔
120
            if plot_type == 'bar':
1✔
121
                kwargs.update({'marker_color': color or 'DarkSlateGrey',
×
122
                               'marker_line_color': color or 'DarkSlateGrey'})
123
            else:
124
                if color:
1✔
125
                    kwargs.update({'line': {'color': color}})
1✔
126
                kwargs['mode'] = 'lines'
1✔
127
                if plot_type != 'scatter':
1✔
128
                    logger.warning(f'Indicator {indicator} has unknown plot trace kind {plot_type}'
×
129
                                   f', assuming "scatter".')
130

131
            kwargs.update(conf.get('plotly', {}))
1✔
132
            trace = plot_kinds[plot_type](**kwargs)
1✔
133
            fig.add_trace(trace, row, 1)
1✔
134
        else:
135
            logger.info(
1✔
136
                'Indicator "%s" ignored. Reason: This indicator is not found '
137
                'in your strategy.',
138
                indicator
139
            )
140

141
    return fig
1✔
142

143

144
def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots:
1✔
145
    """
146
    Add profit-plot
147
    :param fig: Plot figure to append to
148
    :param row: row number for this plot
149
    :param data: candlestick DataFrame
150
    :param column: Column to use for plot
151
    :param name: Name to use
152
    :return: fig with added profit plot
153
    """
154
    profit = go.Scatter(
1✔
155
        x=data.index,
156
        y=data[column],
157
        name=name,
158
    )
159
    fig.add_trace(profit, row, 1)
1✔
160

161
    return fig
1✔
162

163

164
def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
1✔
165
                     timeframe: str, starting_balance: float) -> make_subplots:
166
    """
167
    Add scatter points indicating max drawdown
168
    """
169
    try:
1✔
170
        _, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(
1✔
171
            trades,
172
            starting_balance=starting_balance
173
        )
174

175
        drawdown = go.Scatter(
1✔
176
            x=[highdate, lowdate],
177
            y=[
178
                df_comb.loc[timeframe_to_prev_date(timeframe, highdate), 'cum_profit'],
179
                df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), 'cum_profit'],
180
            ],
181
            mode='markers',
182
            name=f"Max drawdown {max_drawdown:.2%}",
183
            text=f"Max drawdown {max_drawdown:.2%}",
184
            marker=dict(
185
                symbol='square-open',
186
                size=9,
187
                line=dict(width=2),
188
                color='green'
189

190
            )
191
        )
192
        fig.add_trace(drawdown, row, 1)
1✔
193
    except ValueError:
×
194
        logger.warning("No trades found - not plotting max drawdown.")
×
195
    return fig
1✔
196

197

198
def add_underwater(fig, row, trades: pd.DataFrame, starting_balance: float) -> make_subplots:
1✔
199
    """
200
    Add underwater plots
201
    """
202
    try:
1✔
203
        underwater = calculate_underwater(
1✔
204
            trades,
205
            value_col="profit_abs",
206
            starting_balance=starting_balance
207
        )
208

209
        underwater_plot = go.Scatter(
1✔
210
            x=underwater['date'],
211
            y=underwater['drawdown'],
212
            name="Underwater Plot",
213
            fill='tozeroy',
214
            fillcolor='#cc362b',
215
            line={'color': '#cc362b'}
216
        )
217

218
        underwater_plot_relative = go.Scatter(
1✔
219
            x=underwater['date'],
220
            y=(-underwater['drawdown_relative']),
221
            name="Underwater Plot (%)",
222
            fill='tozeroy',
223
            fillcolor='green',
224
            line={'color': 'green'}
225
        )
226

227
        fig.add_trace(underwater_plot, row, 1)
1✔
228
        fig.add_trace(underwater_plot_relative, row + 1, 1)
1✔
229
    except ValueError:
×
230
        logger.warning("No trades found - not plotting underwater plot")
×
231
    return fig
1✔
232

233

234
def add_parallelism(fig, row, trades: pd.DataFrame, timeframe: str) -> make_subplots:
1✔
235
    """
236
    Add Chart showing trade parallelism
237
    """
238
    try:
1✔
239
        result = analyze_trade_parallelism(trades, timeframe)
1✔
240

241
        drawdown = go.Scatter(
1✔
242
            x=result.index,
243
            y=result['open_trades'],
244
            name="Parallel trades",
245
            fill='tozeroy',
246
            fillcolor='#242222',
247
            line={'color': '#242222'},
248
        )
249
        fig.add_trace(drawdown, row, 1)
1✔
250
    except ValueError:
×
251
        logger.warning("No trades found - not plotting Parallelism.")
×
252
    return fig
1✔
253

254

255
def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
1✔
256
    """
257
    Add trades to "fig"
258
    """
259
    # Trades can be empty
260
    if trades is not None and len(trades) > 0:
1✔
261
        # Create description for exit summarizing the trade
262
        trades['desc'] = trades.apply(
1✔
263
            lambda row: f"{row['profit_ratio']:.2%}, " +
264
            (f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") +
265
            f"{row['exit_reason']}, " +
266
            f"{row['trade_duration']} min",
267
            axis=1)
268
        trade_entries = go.Scatter(
1✔
269
            x=trades["open_date"],
270
            y=trades["open_rate"],
271
            mode='markers',
272
            name='Trade entry',
273
            text=trades["desc"],
274
            marker=dict(
275
                symbol='circle-open',
276
                size=11,
277
                line=dict(width=2),
278
                color='cyan'
279

280
            )
281
        )
282

283
        trade_exits = go.Scatter(
1✔
284
            x=trades.loc[trades['profit_ratio'] > 0, "close_date"],
285
            y=trades.loc[trades['profit_ratio'] > 0, "close_rate"],
286
            text=trades.loc[trades['profit_ratio'] > 0, "desc"],
287
            mode='markers',
288
            name='Exit - Profit',
289
            marker=dict(
290
                symbol='square-open',
291
                size=11,
292
                line=dict(width=2),
293
                color='green'
294
            )
295
        )
296
        trade_exits_loss = go.Scatter(
1✔
297
            x=trades.loc[trades['profit_ratio'] <= 0, "close_date"],
298
            y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"],
299
            text=trades.loc[trades['profit_ratio'] <= 0, "desc"],
300
            mode='markers',
301
            name='Exit - Loss',
302
            marker=dict(
303
                symbol='square-open',
304
                size=11,
305
                line=dict(width=2),
306
                color='red'
307
            )
308
        )
309
        fig.add_trace(trade_entries, 1, 1)
1✔
310
        fig.add_trace(trade_exits, 1, 1)
1✔
311
        fig.add_trace(trade_exits_loss, 1, 1)
1✔
312
    else:
313
        logger.warning("No trades found.")
1✔
314
    return fig
1✔
315

316

317
def create_plotconfig(indicators1: List[str], indicators2: List[str],
1✔
318
                      plot_config: Dict[str, Dict]) -> Dict[str, Dict]:
319
    """
320
    Combines indicators 1 and indicators 2 into plot_config if necessary
321
    :param indicators1: List containing Main plot indicators
322
    :param indicators2: List containing Sub plot indicators
323
    :param plot_config: Dict of Dicts containing advanced plot configuration
324
    :return: plot_config - eventually with indicators 1 and 2
325
    """
326

327
    if plot_config:
1✔
328
        if indicators1:
1✔
329
            plot_config['main_plot'] = {ind: {} for ind in indicators1}
1✔
330
        if indicators2:
1✔
331
            plot_config['subplots'] = {'Other': {ind: {} for ind in indicators2}}
1✔
332

333
    if not plot_config:
1✔
334
        # If no indicators and no plot-config given, use defaults.
335
        if not indicators1:
1✔
336
            indicators1 = ['sma', 'ema3', 'ema5']
1✔
337
        if not indicators2:
1✔
338
            indicators2 = ['macd', 'macdsignal']
1✔
339

340
        # Create subplot configuration if plot_config is not available.
341
        plot_config = {
1✔
342
            'main_plot': {ind: {} for ind in indicators1},
343
            'subplots': {'Other': {ind: {} for ind in indicators2}},
344
        }
345
    if 'main_plot' not in plot_config:
1✔
346
        plot_config['main_plot'] = {}
1✔
347

348
    if 'subplots' not in plot_config:
1✔
349
        plot_config['subplots'] = {}
1✔
350
    return plot_config
1✔
351

352

353
def plot_area(fig, row: int, data: pd.DataFrame, indicator_a: str,
1✔
354
              indicator_b: str, label: str = "",
355
              fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots:
356
    """ Creates a plot for the area between two traces and adds it to fig.
357
    :param fig: Plot figure to append to
358
    :param row: row number for this plot
359
    :param data: candlestick DataFrame
360
    :param indicator_a: indicator name as populated in strategy
361
    :param indicator_b: indicator name as populated in strategy
362
    :param label: label for the filled area
363
    :param fill_color: color to be used for the filled area
364
    :return: fig with added  filled_traces plot
365
    """
366
    if indicator_a in data and indicator_b in data:
1✔
367
        # make lines invisible to get the area plotted, only.
368
        line = {'color': 'rgba(255,255,255,0)'}
1✔
369
        # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284
370
        trace_a = go.Scatter(x=data.date, y=data[indicator_a],
1✔
371
                             showlegend=False,
372
                             line=line)
373
        trace_b = go.Scatter(x=data.date, y=data[indicator_b], name=label,
1✔
374
                             fill="tonexty", fillcolor=fill_color,
375
                             line=line)
376
        fig.add_trace(trace_a, row, 1)
1✔
377
        fig.add_trace(trace_b, row, 1)
1✔
378
    return fig
1✔
379

380

381
def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots:
1✔
382
    """ Adds all area plots (specified in plot_config) to fig.
383
    :param fig: Plot figure to append to
384
    :param row: row number for this plot
385
    :param data: candlestick DataFrame
386
    :param indicators: dict with indicators. ie.: plot_config['main_plot'] or
387
                            plot_config['subplots'][subplot_label]
388
    :return: fig with added  filled_traces plot
389
    """
390
    for indicator, ind_conf in indicators.items():
1✔
391
        if 'fill_to' in ind_conf:
1✔
392
            indicator_b = ind_conf['fill_to']
1✔
393
            if indicator in data and indicator_b in data:
1✔
394
                label = ind_conf.get('fill_label',
1✔
395
                                     f'{indicator}<>{indicator_b}')
396
                fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)')
1✔
397
                fig = plot_area(fig, row, data, indicator, indicator_b,
1✔
398
                                label=label, fill_color=fill_color)
399
            elif indicator not in data:
1✔
400
                logger.info(
1✔
401
                    'Indicator "%s" ignored. Reason: This indicator is not '
402
                    'found in your strategy.', indicator
403
                )
404
            elif indicator_b not in data:
1✔
405
                logger.info(
1✔
406
                    'fill_to: "%s" ignored. Reason: This indicator is not '
407
                    'in your strategy.', indicator_b
408
                )
409
    return fig
1✔
410

411

412
def create_scatter(
1✔
413
    data,
414
    column_name,
415
    color,
416
    direction
417
) -> Optional[go.Scatter]:
418

419
    if column_name in data.columns:
1✔
420
        df_short = data[data[column_name] == 1]
1✔
421
        if len(df_short) > 0:
1✔
422
            shorts = go.Scatter(
1✔
423
                x=df_short.date,
424
                y=df_short.close,
425
                mode='markers',
426
                name=column_name,
427
                marker=dict(
428
                    symbol=f"triangle-{direction}-dot",
429
                    size=9,
430
                    line=dict(width=1),
431
                    color=color,
432
                )
433
            )
434
            return shorts
1✔
435
        else:
436
            logger.warning(f"No {column_name}-signals found.")
1✔
437

438
    return None
1✔
439

440

441
def generate_candlestick_graph(
1✔
442
        pair: str, data: pd.DataFrame, trades: Optional[pd.DataFrame] = None, *,
443
        indicators1: Optional[List[str]] = None, indicators2: Optional[List[str]] = None,
444
        plot_config: Optional[Dict[str, Dict]] = None,
445
        ) -> go.Figure:
446
    """
447
    Generate the graph from the data generated by Backtesting or from DB
448
    Volume will always be plotted in row2, so Row 1 and 3 are to our disposal for custom indicators
449
    :param pair: Pair to Display on the graph
450
    :param data: OHLCV DataFrame containing indicators and entry/exit signals
451
    :param trades: All trades created
452
    :param indicators1: List containing Main plot indicators
453
    :param indicators2: List containing Sub plot indicators
454
    :param plot_config: Dict of Dicts containing advanced plot configuration
455
    :return: Plotly figure
456
    """
457
    plot_config = create_plotconfig(
1✔
458
        indicators1 or [],
459
        indicators2 or [],
460
        plot_config or {},
461
    )
462
    rows = 2 + len(plot_config['subplots'])
1✔
463
    row_widths = [1 for _ in plot_config['subplots']]
1✔
464
    # Define the graph
465
    fig = make_subplots(
1✔
466
        rows=rows,
467
        cols=1,
468
        shared_xaxes=True,
469
        row_width=row_widths + [1, 4],
470
        vertical_spacing=0.0001,
471
    )
472
    fig['layout'].update(title=pair)
1✔
473
    fig['layout']['yaxis1'].update(title='Price')
1✔
474
    fig['layout']['yaxis2'].update(title='Volume')
1✔
475
    for i, name in enumerate(plot_config['subplots']):
1✔
476
        fig['layout'][f'yaxis{3 + i}'].update(title=name)
1✔
477
    fig['layout']['xaxis']['rangeslider'].update(visible=False)
1✔
478
    fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
1✔
479

480
    # Common information
481
    candles = go.Candlestick(
1✔
482
        x=data.date,
483
        open=data.open,
484
        high=data.high,
485
        low=data.low,
486
        close=data.close,
487
        name='Price'
488
    )
489
    fig.add_trace(candles, 1, 1)
1✔
490

491
    longs = create_scatter(data, 'enter_long', 'green', 'up')
1✔
492
    exit_longs = create_scatter(data, 'exit_long', 'red', 'down')
1✔
493
    shorts = create_scatter(data, 'enter_short', 'blue', 'down')
1✔
494
    exit_shorts = create_scatter(data, 'exit_short', 'violet', 'up')
1✔
495

496
    for scatter in [longs, exit_longs, shorts, exit_shorts]:
1✔
497
        if scatter:
1✔
498
            fig.add_trace(scatter, 1, 1)
1✔
499

500
    # Add Bollinger Bands
501
    fig = plot_area(fig, 1, data, 'bb_lowerband', 'bb_upperband',
1✔
502
                    label="Bollinger Band")
503
    # prevent bb_lower and bb_upper from plotting
504
    try:
1✔
505
        del plot_config['main_plot']['bb_lowerband']
1✔
506
        del plot_config['main_plot']['bb_upperband']
×
507
    except KeyError:
1✔
508
        pass
1✔
509
    # main plot goes to row 1
510
    fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data)
1✔
511
    fig = add_areas(fig, 1, data, plot_config['main_plot'])
1✔
512
    fig = plot_trades(fig, trades)
1✔
513
    # sub plot: Volume goes to row 2
514
    volume = go.Bar(
1✔
515
        x=data['date'],
516
        y=data['volume'],
517
        name='Volume',
518
        marker_color='DarkSlateGrey',
519
        marker_line_color='DarkSlateGrey'
520
    )
521
    fig.add_trace(volume, 2, 1)
1✔
522
    # add each sub plot to a separate row
523
    for i, label in enumerate(plot_config['subplots']):
1✔
524
        sub_config = plot_config['subplots'][label]
1✔
525
        row = 3 + i
1✔
526
        fig = add_indicators(fig=fig, row=row, indicators=sub_config,
1✔
527
                             data=data)
528
        # fill area between indicators ( 'fill_to': 'other_indicator')
529
        fig = add_areas(fig, row, data, sub_config)
1✔
530

531
    return fig
1✔
532

533

534
def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
1✔
535
                          trades: pd.DataFrame, timeframe: str, stake_currency: str,
536
                          starting_balance: float) -> go.Figure:
537
    # Combine close-values for all pairs, rename columns to "pair"
538
    try:
1✔
539
        df_comb = combine_dataframes_with_mean(data, "close")
1✔
540
    except ValueError:
×
541
        raise OperationalException(
×
542
            "No data found. Please make sure that data is available for "
543
            "the timerange and pairs selected.")
544

545
    # Trim trades to available OHLCV data
546
    trades = extract_trades_of_period(df_comb, trades, date_index=True)
1✔
547
    if len(trades) == 0:
1✔
548
        raise OperationalException('No trades found in selected timerange.')
1✔
549

550
    # Add combined cumulative profit
551
    df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe)
1✔
552

553
    # Plot the pairs average close prices, and total profit growth
554
    avgclose = go.Scatter(
1✔
555
        x=df_comb.index,
556
        y=df_comb['mean'],
557
        name='Avg close price',
558
    )
559

560
    fig = make_subplots(rows=6, cols=1, shared_xaxes=True,
1✔
561
                        row_heights=[1, 1, 1, 0.5, 0.75, 0.75],
562
                        vertical_spacing=0.05,
563
                        subplot_titles=[
564
                            "AVG Close Price",
565
                            "Combined Profit",
566
                            "Profit per pair",
567
                            "Parallelism",
568
                            "Underwater",
569
                            "Relative Drawdown",
570
                        ])
571
    fig['layout'].update(title="Freqtrade Profit plot")
1✔
572
    fig['layout']['yaxis1'].update(title='Price')
1✔
573
    fig['layout']['yaxis2'].update(title=f'Profit {stake_currency}')
1✔
574
    fig['layout']['yaxis3'].update(title=f'Profit {stake_currency}')
1✔
575
    fig['layout']['yaxis4'].update(title='Trade count')
1✔
576
    fig['layout']['yaxis5'].update(title='Underwater Plot')
1✔
577
    fig['layout']['yaxis6'].update(title='Underwater Plot Relative (%)', tickformat=',.2%')
1✔
578
    fig['layout']['xaxis']['rangeslider'].update(visible=False)
1✔
579
    fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
1✔
580

581
    fig.add_trace(avgclose, 1, 1)
1✔
582
    fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
1✔
583
    fig = add_max_drawdown(fig, 2, trades, df_comb, timeframe, starting_balance)
1✔
584
    fig = add_parallelism(fig, 4, trades, timeframe)
1✔
585
    # Two rows consumed
586
    fig = add_underwater(fig, 5, trades, starting_balance)
1✔
587

588
    for pair in pairs:
1✔
589
        profit_col = f'cum_profit_{pair}'
1✔
590
        try:
1✔
591
            df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col,
1✔
592
                                        timeframe)
593
            fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}")
1✔
594
        except ValueError:
×
595
            pass
×
596
    return fig
1✔
597

598

599
def generate_plot_filename(pair: str, timeframe: str) -> str:
1✔
600
    """
601
    Generate filenames per pair/timeframe to be used for storing plots
602
    """
603
    pair_s = pair_to_filename(pair)
1✔
604
    file_name = 'freqtrade-plot-' + pair_s + '-' + timeframe + '.html'
1✔
605

606
    logger.info('Generate plot file for %s', pair)
1✔
607

608
    return file_name
1✔
609

610

611
def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False) -> None:
1✔
612
    """
613
    Generate a plot html file from pre populated fig plotly object
614
    :param fig: Plotly Figure to plot
615
    :param filename: Name to store the file as
616
    :param directory: Directory to store the file in
617
    :param auto_open: Automatically open files saved
618
    :return: None
619
    """
620
    directory.mkdir(parents=True, exist_ok=True)
1✔
621

622
    _filename = directory.joinpath(filename)
1✔
623
    plot(fig, filename=str(_filename),
1✔
624
         auto_open=auto_open)
625
    logger.info(f"Stored plot as {_filename}")
1✔
626

627

628
def load_and_plot_trades(config: Config):
1✔
629
    """
630
    From configuration provided
631
    - Initializes plot-script
632
    - Get candle (OHLCV) data
633
    - Generate Dafaframes populated with indicators and signals based on configured strategy
634
    - Load trades executed during the selected period
635
    - Generate Plotly plot objects
636
    - Generate plot files
637
    :return: None
638
    """
639
    strategy = StrategyResolver.load_strategy(config)
1✔
640

641
    exchange = ExchangeResolver.load_exchange(config)
1✔
642
    IStrategy.dp = DataProvider(config, exchange)
1✔
643
    strategy.ft_bot_start()
1✔
644
    strategy_safe_wrapper(strategy.bot_loop_start)(current_time=datetime.now(timezone.utc))
1✔
645
    plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
1✔
646
    timerange = plot_elements['timerange']
1✔
647
    trades = plot_elements['trades']
1✔
648
    pair_counter = 0
1✔
649
    for pair, data in plot_elements["ohlcv"].items():
1✔
650
        pair_counter += 1
1✔
651
        logger.info("analyse pair %s", pair)
1✔
652

653
        df_analyzed = strategy.analyze_ticker(data, {'pair': pair})
1✔
654
        df_analyzed = trim_dataframe(df_analyzed, timerange)
1✔
655
        if not trades.empty:
1✔
656
            trades_pair = trades.loc[trades['pair'] == pair]
×
657
            trades_pair = extract_trades_of_period(df_analyzed, trades_pair)
×
658
        else:
659
            trades_pair = trades
1✔
660

661
        fig = generate_candlestick_graph(
1✔
662
            pair=pair,
663
            data=df_analyzed,
664
            trades=trades_pair,
665
            indicators1=config.get('indicators1', []),
666
            indicators2=config.get('indicators2', []),
667
            plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {}
668
        )
669

670
        store_plot_file(fig, filename=generate_plot_filename(pair, config['timeframe']),
1✔
671
                        directory=config['user_data_dir'] / 'plot')
672

673
    logger.info('End of plotting process. %s plots generated', pair_counter)
1✔
674

675

676
def plot_profit(config: Config) -> None:
1✔
677
    """
678
    Plots the total profit for all pairs.
679
    Note, the profit calculation isn't realistic.
680
    But should be somewhat proportional, and therefore useful
681
    in helping out to find a good algorithm.
682
    """
683
    if 'timeframe' not in config:
1✔
684
        raise OperationalException('Timeframe must be set in either config or via --timeframe.')
1✔
685

686
    exchange = ExchangeResolver.load_exchange(config)
1✔
687
    plot_elements = init_plotscript(config, list(exchange.markets))
1✔
688
    trades = plot_elements['trades']
1✔
689
    # Filter trades to relevant pairs
690
    # Remove open pairs - we don't know the profit yet so can't calculate profit for these.
691
    # Also, If only one open pair is left, then the profit-generation would fail.
692
    trades = trades[(trades['pair'].isin(plot_elements['pairs']))
1✔
693
                    & (~trades['close_date'].isnull())
694
                    ]
695
    if len(trades) == 0:
1✔
696
        raise OperationalException("No trades found, cannot generate Profit-plot without "
1✔
697
                                   "trades from either Backtest result or database.")
698

699
    # Create an average close price of all the pairs that were involved.
700
    # this could be useful to gauge the overall market trend
701
    fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'],
1✔
702
                                trades, config['timeframe'],
703
                                config.get('stake_currency', ''),
704
                                config.get('available_capital', config['dry_run_wallet']))
705
    store_plot_file(fig, filename='freqtrade-profit-plot.html',
1✔
706
                    directory=config['user_data_dir'] / 'plot',
707
                    auto_open=config.get('plot_auto_open', False))
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