diff --git a/docs/plotting.md b/docs/plotting.md index 09eb6ddb5..ed682e44b 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -168,6 +168,7 @@ Additional features when using plot_config include: * Specify colors per indicator * Specify additional subplots +* Specify indicator pairs to fill area in between The sample plot configuration below specifies fixed colors for the indicators. Otherwise consecutive plots may produce different colorschemes each time, making comparisons difficult. It also allows multiple subplots to display both MACD and RSI at the same time. @@ -183,23 +184,33 @@ Sample configuration with inline comments explaining the process: 'ema50': {'color': '#CCCCCC'}, # By omitting color, a random color is selected. 'sar': {}, + # fill area between senkou_a and senkou_b + 'senkou_a': { + 'color': 'green', #optional + 'fill_to': 'senkou_b', + 'fill_label': 'Ichimoku Cloud' #optional, + 'fill_color': 'rgba(255,76,46,0.2)', #optional + }, + # plot senkou_b, too. Not only the area to it. + 'senkou_b': {} }, 'subplots': { # Create subplot MACD "MACD": { - 'macd': {'color': 'blue'}, - 'macdsignal': {'color': 'orange'}, + 'macd': {'color': 'blue', 'fill_to': 'macdhist'}, + 'macdsignal': {'color': 'orange'} }, # Additional subplot RSI "RSI": { - 'rsi': {'color': 'red'}, + 'rsi': {'color': 'red'} } } } -``` +``` !!! Note - The above configuration assumes that `ema10`, `ema50`, `macd`, `macdsignal` and `rsi` are columns in the DataFrame created by the strategy. + The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, + `macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy. ## Plot profit diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f7d300593..497218deb 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -263,6 +263,65 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], return plot_config +def plot_area(fig, row: int, data: pd.DataFrame, indicator_a: str, + indicator_b: str, label: str = "", + fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: + """ Creates a plot for the area between two traces and adds it to fig. + :param fig: Plot figure to append to + :param row: row number for this plot + :param data: candlestick DataFrame + :param indicator_a: indicator name as populated in stragetie + :param indicator_b: indicator name as populated in stragetie + :param label: label for the filled area + :param fill_color: color to be used for the filled area + :return: fig with added filled_traces plot + """ + if indicator_a in data and indicator_b in data: + # make lines invisible to get the area plotted, only. + line = {'color': 'rgba(255,255,255,0)'} + # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284 + trace_a = go.Scatter(x=data.date, y=data[indicator_a], + showlegend=False, + line=line) + trace_b = go.Scatter(x=data.date, y=data[indicator_b], name=label, + fill="tonexty", fillcolor=fill_color, + line=line) + fig.add_trace(trace_a, row, 1) + fig.add_trace(trace_b, row, 1) + return fig + + +def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: + """ Adds all area plots (specified in plot_config) to fig. + :param fig: Plot figure to append to + :param row: row number for this plot + :param data: candlestick DataFrame + :param indicators: dict with indicators. ie.: plot_config['main_plot'] or + plot_config['subplots'][subplot_label] + :return: fig with added filled_traces plot + """ + for indicator, ind_conf in indicators.items(): + if 'fill_to' in ind_conf: + indicator_b = ind_conf['fill_to'] + if indicator in data and indicator_b in data: + label = ind_conf.get('fill_label', + f'{indicator}<>{indicator_b}') + fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') + fig = plot_area(fig, row, data, indicator, indicator_b, + label=label, fill_color=fill_color) + elif indicator not in data: + logger.info( + 'Indicator "%s" ignored. Reason: This indicator is not ' + 'found in your strategy.', indicator + ) + elif indicator_b not in data: + logger.info( + 'fill_to: "%s" ignored. Reason: This indicator is not ' + 'in your strategy.', indicator_b + ) + return fig + + def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, indicators1: List[str] = [], indicators2: List[str] = [], @@ -280,7 +339,6 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra :return: Plotly figure """ plot_config = create_plotconfig(indicators1, indicators2, plot_config) - rows = 2 + len(plot_config['subplots']) row_widths = [1 for _ in plot_config['subplots']] # Define the graph @@ -346,36 +404,20 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra fig.add_trace(sells, 1, 1) else: logger.warning("No sell-signals found.") - - # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284 - if 'bb_lowerband' in data and 'bb_upperband' in data: - bb_lower = go.Scatter( - x=data.date, - y=data.bb_lowerband, - showlegend=False, - line={'color': 'rgba(255,255,255,0)'}, - ) - bb_upper = go.Scatter( - x=data.date, - y=data.bb_upperband, - name='Bollinger Band', - fill="tonexty", - fillcolor="rgba(0,176,246,0.2)", - line={'color': 'rgba(255,255,255,0)'}, - ) - fig.add_trace(bb_lower, 1, 1) - fig.add_trace(bb_upper, 1, 1) - if ('bb_upperband' in plot_config['main_plot'] - and 'bb_lowerband' in plot_config['main_plot']): - del plot_config['main_plot']['bb_upperband'] - del plot_config['main_plot']['bb_lowerband'] - - # Add indicators to main plot + # Add Bollinger Bands + fig = plot_area(fig, 1, data, 'bb_lowerband', 'bb_upperband', + label="Bollinger Band") + # prevent bb_lower and bb_upper from plotting + try: + del plot_config['main_plot']['bb_lowerband'] + del plot_config['main_plot']['bb_upperband'] + except KeyError: + pass + # main plot goes to row 1 fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) - + fig = add_areas(fig, 1, data, plot_config['main_plot']) fig = plot_trades(fig, trades) - - # Volume goes to row 2 + # sub plot: Volume goes to row 2 volume = go.Bar( x=data['date'], y=data['volume'], @@ -384,13 +426,14 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra marker_line_color='DarkSlateGrey' ) fig.add_trace(volume, 2, 1) - - # Add indicators to separate row - for i, name in enumerate(plot_config['subplots']): - fig = add_indicators(fig=fig, row=3 + i, - indicators=plot_config['subplots'][name], + # add each sub plot to a separate row + for i, label in enumerate(plot_config['subplots']): + sub_config = plot_config['subplots'][label] + row = 3 + i + fig = add_indicators(fig=fig, row=row, indicators=sub_config, data=data) - + # fill area between indicators ( 'fill_to': 'other_indicator') + fig = add_areas(fig, row, data, sub_config) return fig diff --git a/tests/test_plotting.py b/tests/test_plotting.py index d3f97013d..42847ca50 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -13,7 +13,7 @@ from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.exceptions import OperationalException -from freqtrade.plot.plotting import (add_indicators, add_profit, create_plotconfig, +from freqtrade.plot.plotting import (add_areas, add_indicators, add_profit, create_plotconfig, generate_candlestick_graph, generate_plot_filename, generate_profit_graph, init_plotscript, load_and_plot_trades, plot_profit, plot_trades, store_plot_file) @@ -96,6 +96,62 @@ def test_add_indicators(default_conf, testdatadir, caplog): assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog) +def test_add_areas(default_conf, testdatadir, caplog): + pair = "UNITTEST/BTC" + timerange = TimeRange(None, 'line', 0, -1000) + + data = history.load_pair_history(pair=pair, timeframe='1m', + datadir=testdatadir, timerange=timerange) + indicators = {"macd": {"color": "red", + "fill_color": "black", + "fill_to": "macdhist", + "fill_label": "MACD Fill"}} + + ind_no_label = {"macd": {"fill_color": "red", + "fill_to": "macdhist"}} + + ind_plain = {"macd": {"fill_to": "macdhist"}} + default_conf.update({'strategy': 'DefaultStrategy'}) + strategy = StrategyResolver.load_strategy(default_conf) + + # Generate buy/sell signals and indicators + data = strategy.analyze_ticker(data, {'pair': pair}) + fig = generate_empty_figure() + + # indicator mentioned in fill_to does not exist + fig1 = add_areas(fig, 1, data, {'ema10': {'fill_to': 'no_fill_indicator'}}) + assert fig == fig1 + assert log_has_re(r'fill_to: "no_fill_indicator" ignored\..*', caplog) + + # indicator does not exist + fig2 = add_areas(fig, 1, data, {'no_indicator': {'fill_to': 'ema10'}}) + assert fig == fig2 + assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog) + + # everythin given in plot config, row 3 + fig3 = add_areas(fig, 3, data, indicators) + figure = fig3.layout.figure + fill_macd = find_trace_in_fig_data(figure.data, "MACD Fill") + assert isinstance(fill_macd, go.Scatter) + assert fill_macd.yaxis == "y3" + assert fill_macd.fillcolor == "black" + + # label missing, row 1 + fig4 = add_areas(fig, 1, data, ind_no_label) + figure = fig4.layout.figure + fill_macd = find_trace_in_fig_data(figure.data, "macd<>macdhist") + assert isinstance(fill_macd, go.Scatter) + assert fill_macd.yaxis == "y" + assert fill_macd.fillcolor == "red" + + # fit_to only + fig5 = add_areas(fig, 1, data, ind_plain) + figure = fig5.layout.figure + fill_macd = find_trace_in_fig_data(figure.data, "macd<>macdhist") + assert isinstance(fill_macd, go.Scatter) + assert fill_macd.yaxis == "y" + + def test_plot_trades(testdatadir, caplog): fig1 = generate_empty_figure() # nothing happens when no trades are available