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

freqtrade / freqtrade / 13299668853

03 Jan 2025 02:29PM UTC coverage: 94.322% (-0.09%) from 94.407%
13299668853

push

github

xmatthias
test: add test for improved safe_wrapper behavior

21793 of 23105 relevant lines covered (94.32%)

0.94 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

5
import pandas as pd
1✔
6

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

33

34
logger = logging.getLogger(__name__)
1✔
35

36

37
try:
1✔
38
    import plotly.graph_objects as go
1✔
39
    from plotly.offline import plot
1✔
40
    from plotly.subplots import make_subplots
1✔
41
except ImportError:
×
42
    logger.exception("Module plotly not found \n Please install using `pip3 install plotly`")
×
43
    exit(1)
×
44

45

46
def init_plotscript(config, markets: list, startup_candles: int = 0):
1✔
47
    """
48
    Initialize objects needed for plotting
49
    :return: Dict with candle (OHLCV) data, trades and pairs
50
    """
51

52
    if "pairs" in config:
1✔
53
        pairs = expand_pairlist(config["pairs"], markets)
1✔
54
    else:
55
        pairs = expand_pairlist(config["exchange"]["pair_whitelist"], markets)
1✔
56

57
    # Set timerange to use
58
    timerange = TimeRange.parse_timerange(config.get("timerange"))
1✔
59

60
    data = load_data(
1✔
61
        datadir=config.get("datadir"),
62
        pairs=pairs,
63
        timeframe=config["timeframe"],
64
        timerange=timerange,
65
        startup_candles=startup_candles,
66
        data_format=config["dataformat_ohlcv"],
67
        candle_type=config.get("candle_type_def", CandleType.SPOT),
68
    )
69

70
    if startup_candles and data:
1✔
71
        min_date, max_date = get_timerange(data)
1✔
72
        logger.info(f"Loading data from {min_date} to {max_date}")
1✔
73
        timerange.adjust_start_if_necessary(
1✔
74
            timeframe_to_seconds(config["timeframe"]), startup_candles, min_date
75
        )
76

77
    no_trades = False
1✔
78
    filename = config.get("exportfilename")
1✔
79
    if config.get("no_trades", False):
1✔
80
        no_trades = True
×
81
    elif config["trade_source"] == "file":
1✔
82
        if not filename.is_dir() and not filename.is_file():
1✔
83
            logger.warning("Backtest file is missing skipping trades.")
1✔
84
            no_trades = True
1✔
85
    try:
1✔
86
        trades = load_trades(
1✔
87
            config["trade_source"],
88
            db_url=config.get("db_url"),
89
            exportfilename=filename,
90
            no_trades=no_trades,
91
            strategy=config.get("strategy"),
92
        )
93
    except ValueError as e:
×
94
        raise OperationalException(e) from e
×
95
    if not trades.empty:
1✔
96
        trades = trim_dataframe(trades, timerange, df_date_col="open_date")
1✔
97

98
    return {
1✔
99
        "ohlcv": data,
100
        "trades": trades,
101
        "pairs": pairs,
102
        "timerange": timerange,
103
    }
104

105

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

124
            plot_type = conf.get("type", "scatter")
1✔
125
            color = conf.get("color")
1✔
126
            if plot_type == "bar":
1✔
127
                kwargs.update(
×
128
                    {
129
                        "marker_color": color or "DarkSlateGrey",
130
                        "marker_line_color": color or "DarkSlateGrey",
131
                    }
132
                )
133
            else:
134
                if color:
1✔
135
                    kwargs.update({"line": {"color": color}})
1✔
136
                kwargs["mode"] = "lines"
1✔
137
                if plot_type != "scatter":
1✔
138
                    logger.warning(
×
139
                        f"Indicator {indicator} has unknown plot trace kind {plot_type}"
140
                        f', assuming "scatter".'
141
                    )
142

143
            kwargs.update(conf.get("plotly", {}))
1✔
144
            trace = plot_kinds[plot_type](**kwargs)
1✔
145
            fig.add_trace(trace, row, 1)
1✔
146
        else:
147
            logger.info(
1✔
148
                'Indicator "%s" ignored. Reason: This indicator is not found ' "in your strategy.",
149
                indicator,
150
            )
151

152
    return fig
1✔
153

154

155
def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots:
1✔
156
    """
157
    Add profit-plot
158
    :param fig: Plot figure to append to
159
    :param row: row number for this plot
160
    :param data: candlestick DataFrame
161
    :param column: Column to use for plot
162
    :param name: Name to use
163
    :return: fig with added profit plot
164
    """
165
    profit = go.Scatter(
1✔
166
        x=data.index,
167
        y=data[column],
168
        name=name,
169
    )
170
    fig.add_trace(profit, row, 1)
1✔
171

172
    return fig
1✔
173

174

175
def add_max_drawdown(
1✔
176
    fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, timeframe: str, starting_balance: float
177
) -> make_subplots:
178
    """
179
    Add scatter points indicating max drawdown
180
    """
181
    try:
1✔
182
        drawdown = calculate_max_drawdown(trades, starting_balance=starting_balance)
1✔
183

184
        drawdown = go.Scatter(
1✔
185
            x=[drawdown.high_date, drawdown.low_date],
186
            y=[
187
                df_comb.loc[timeframe_to_prev_date(timeframe, drawdown.high_date), "cum_profit"],
188
                df_comb.loc[timeframe_to_prev_date(timeframe, drawdown.low_date), "cum_profit"],
189
            ],
190
            mode="markers",
191
            name=f"Max drawdown {drawdown.relative_account_drawdown:.2%}",
192
            text=f"Max drawdown {drawdown.relative_account_drawdown:.2%}",
193
            marker=dict(symbol="square-open", size=9, line=dict(width=2), color="green"),
194
        )
195
        fig.add_trace(drawdown, row, 1)
1✔
196
    except ValueError:
×
197
        logger.warning("No trades found - not plotting max drawdown.")
×
198
    return fig
1✔
199

200

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

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

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

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

234

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

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

255

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

279
        trade_exits = go.Scatter(
1✔
280
            x=trades.loc[trades["profit_ratio"] > 0, "close_date"],
281
            y=trades.loc[trades["profit_ratio"] > 0, "close_rate"],
282
            text=trades.loc[trades["profit_ratio"] > 0, "desc"],
283
            mode="markers",
284
            name="Exit - Profit",
285
            marker=dict(symbol="square-open", size=11, line=dict(width=2), color="green"),
286
        )
287
        trade_exits_loss = go.Scatter(
1✔
288
            x=trades.loc[trades["profit_ratio"] <= 0, "close_date"],
289
            y=trades.loc[trades["profit_ratio"] <= 0, "close_rate"],
290
            text=trades.loc[trades["profit_ratio"] <= 0, "desc"],
291
            mode="markers",
292
            name="Exit - Loss",
293
            marker=dict(symbol="square-open", size=11, line=dict(width=2), color="red"),
294
        )
295
        fig.add_trace(trade_entries, 1, 1)
1✔
296
        fig.add_trace(trade_exits, 1, 1)
1✔
297
        fig.add_trace(trade_exits_loss, 1, 1)
1✔
298
    else:
299
        logger.warning("No trades found.")
1✔
300
    return fig
1✔
301

302

303
def create_plotconfig(
1✔
304
    indicators1: list[str], indicators2: list[str], plot_config: dict[str, dict]
305
) -> dict[str, dict]:
306
    """
307
    Combines indicators 1 and indicators 2 into plot_config if necessary
308
    :param indicators1: List containing Main plot indicators
309
    :param indicators2: List containing Sub plot indicators
310
    :param plot_config: Dict of Dicts containing advanced plot configuration
311
    :return: plot_config - eventually with indicators 1 and 2
312
    """
313

314
    if plot_config:
1✔
315
        if indicators1:
1✔
316
            plot_config["main_plot"] = {ind: {} for ind in indicators1}
1✔
317
        if indicators2:
1✔
318
            plot_config["subplots"] = {"Other": {ind: {} for ind in indicators2}}
1✔
319

320
    if not plot_config:
1✔
321
        # If no indicators and no plot-config given, use defaults.
322
        if not indicators1:
1✔
323
            indicators1 = ["sma", "ema3", "ema5"]
1✔
324
        if not indicators2:
1✔
325
            indicators2 = ["macd", "macdsignal"]
1✔
326

327
        # Create subplot configuration if plot_config is not available.
328
        plot_config = {
1✔
329
            "main_plot": {ind: {} for ind in indicators1},
330
            "subplots": {"Other": {ind: {} for ind in indicators2}},
331
        }
332
    if "main_plot" not in plot_config:
1✔
333
        plot_config["main_plot"] = {}
1✔
334

335
    if "subplots" not in plot_config:
1✔
336
        plot_config["subplots"] = {}
1✔
337
    return plot_config
1✔
338

339

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

376

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

408

409
def create_scatter(data, column_name, color, direction) -> go.Scatter | None:
1✔
410
    if column_name in data.columns:
1✔
411
        df_short = data[data[column_name] == 1]
1✔
412
        if len(df_short) > 0:
1✔
413
            shorts = go.Scatter(
1✔
414
                x=df_short.date,
415
                y=df_short.close,
416
                mode="markers",
417
                name=column_name,
418
                marker=dict(
419
                    symbol=f"triangle-{direction}-dot",
420
                    size=9,
421
                    line=dict(width=1),
422
                    color=color,
423
                ),
424
            )
425
            return shorts
1✔
426
        else:
427
            logger.warning(f"No {column_name}-signals found.")
1✔
428

429
    return None
1✔
430

431

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

475
    # Common information
476
    candles = go.Candlestick(
1✔
477
        x=data.date, open=data.open, high=data.high, low=data.low, close=data.close, name="Price"
478
    )
479
    fig.add_trace(candles, 1, 1)
1✔
480

481
    longs = create_scatter(data, "enter_long", "green", "up")
1✔
482
    exit_longs = create_scatter(data, "exit_long", "red", "down")
1✔
483
    shorts = create_scatter(data, "enter_short", "blue", "down")
1✔
484
    exit_shorts = create_scatter(data, "exit_short", "violet", "up")
1✔
485

486
    for scatter in [longs, exit_longs, shorts, exit_shorts]:
1✔
487
        if scatter:
1✔
488
            fig.add_trace(scatter, 1, 1)
1✔
489

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

519
    return fig
1✔
520

521

522
def generate_profit_graph(
1✔
523
    pairs: str,
524
    data: dict[str, pd.DataFrame],
525
    trades: pd.DataFrame,
526
    timeframe: str,
527
    stake_currency: str,
528
    starting_balance: float,
529
) -> go.Figure:
530
    # Combine close-values for all pairs, rename columns to "pair"
531
    try:
1✔
532
        df_comb = combine_dataframes_with_mean(data, "close")
1✔
533
    except ValueError:
×
534
        raise OperationalException(
×
535
            "No data found. Please make sure that data is available for "
536
            "the timerange and pairs selected."
537
        )
538

539
    # Trim trades to available OHLCV data
540
    trades = extract_trades_of_period(df_comb, trades, date_index=True)
1✔
541
    if len(trades) == 0:
1✔
542
        raise OperationalException("No trades found in selected timerange.")
1✔
543

544
    # Add combined cumulative profit
545
    df_comb = create_cum_profit(df_comb, trades, "cum_profit", timeframe)
1✔
546

547
    # Plot the pairs average close prices, and total profit growth
548
    avgclose = go.Scatter(
1✔
549
        x=df_comb.index,
550
        y=df_comb["mean"],
551
        name="Avg close price",
552
    )
553

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

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

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

597

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

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

607
    return file_name
1✔
608

609

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

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

625

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

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

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

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

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

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

676

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

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

702
    # Create an average close price of all the pairs that were involved.
703
    # this could be useful to gauge the overall market trend
704
    fig = generate_profit_graph(
1✔
705
        plot_elements["pairs"],
706
        plot_elements["ohlcv"],
707
        trades,
708
        config["timeframe"],
709
        config.get("stake_currency", ""),
710
        config.get("available_capital", get_dry_run_wallet(config)),
711
    )
712
    store_plot_file(
1✔
713
        fig,
714
        filename="freqtrade-profit-plot.html",
715
        directory=config["user_data_dir"] / "plot",
716
        auto_open=config.get("plot_auto_open", False),
717
    )
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

© 2026 Coveralls, Inc