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

freqtrade / freqtrade / 15315472728

02 Dec 2024 07:11PM UTC coverage: 94.435% (+0.07%) from 94.365%
15315472728

push

github

web-flow
Merge pull request #11028 from xzmeng/fix-none

fix: check if days is None before conversion

1 of 1 new or added line in 1 file covered. (100.0%)

521 existing lines in 52 files now uncovered.

21687 of 22965 relevant lines covered (94.44%)

0.94 hits per line

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

92.31
/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

32

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

35

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

44

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

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

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

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

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

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

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

104

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

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

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

151
    return fig
1✔
152

153

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

171
    return fig
1✔
172

173

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

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

199

200
def add_underwater(fig, row, trades: pd.DataFrame, starting_balance: float) -> make_subplots:
1✔
201
    """
202
    Add underwater plots
203
    """
204
    try:
1✔
205
        underwater = calculate_underwater(
1✔
206
            trades, value_col="profit_abs", 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✔
UNCOV
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✔
UNCOV
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
        )
269
        trade_entries = go.Scatter(
1✔
270
            x=trades["open_date"],
271
            y=trades["open_rate"],
272
            mode="markers",
273
            name="Trade entry",
274
            text=trades["desc"],
275
            marker=dict(symbol="circle-open", size=11, line=dict(width=2), color="cyan"),
276
        )
277

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

301

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

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

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

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

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

338

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

375

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