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

freqtrade / freqtrade / 16362547577

18 Jul 2025 04:58AM UTC coverage: 94.258% (-0.2%) from 94.415%
16362547577

push

github

xmatthias
docs: add /profit_long and short to telegram docs

22228 of 23582 relevant lines covered (94.26%)

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 UTC, datetime
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 found in your strategy.',
398
                    indicator,
399
                )
400
            elif indicator_b not in data:
1✔
401
                logger.info(
1✔
402
                    'fill_to: "%s" ignored. Reason: This indicator is not in your strategy.',
403
                    indicator_b,
404
                )
405
    return fig
1✔
406

407

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

428
    return None
1✔
429

430

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

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

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

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

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

518
    return fig
1✔
519

520

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

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

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

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

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

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

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

596

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

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

606
    return file_name
1✔
607

608

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

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

624

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

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

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

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

667
        store_plot_file(
1✔
668
            fig,
669
            filename=generate_plot_filename(pair, config["timeframe"]),
670
            directory=config["user_data_dir"] / "plot",
671
        )
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[
1✔
693
        (trades["pair"].isin(plot_elements["pairs"])) & (~trades["close_date"].isnull())
694
    ]
695
    if len(trades) == 0:
1✔
696
        raise OperationalException(
1✔
697
            "No trades found, cannot generate Profit-plot without "
698
            "trades from either Backtest result or database."
699
        )
700

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