backtesting.py icon indicating copy to clipboard operation
backtesting.py copied to clipboard

Histogram indicator plot style

Open unreal639 opened this issue 4 years ago • 8 comments

I am using the method I to plot MACD, but get the wrong style with histogram. Maybe need more style settings with the method.

unreal639 avatar Dec 13 '20 09:12 unreal639

All parameters how to plot one indicator should be just one style parameter as list of dictionaries, one for each element of the indicator. Then you can specify for MACD to have 2 line elements and one histogram with the corresponding colors and names.

I did some experiments and added a complex style parameter where the user can select even size, transparency, type, color, name etc. of each element of one indicator but it needs much more work before proposing this option. Something like this: sample

AGG2017 avatar Aug 13 '21 15:08 AGG2017

@AGG2017 how did you specify the legends for each of the lines in 1 indicator graph? Could you share the code for this? I'm trying to draw MACD signal like this too. Thanks.

wakandan avatar Oct 29 '21 23:10 wakandan

@wakandan check his commits, he change the core of library, to get such results, you should by yourself change 3 times original code. 1)Add histogram feature 2)Add legend feature 3)Add color feature

vladimircape avatar Nov 16 '21 09:11 vladimircape

@wakandan

def MACD_Backtesting_FAST(values, fast):
    """
    Return exponential moving average of `values`, at
    each step taking into account `n` previous values.
    """
    close = pd.Series(values)
    return  close.ewm(span=fast, adjust=False,min_periods=fast).mean()

def MACD_Backtesting_SLOW(values, slow):
    """
    Return exponential moving average of `values`, at
    each step taking into account `n` previous values.
    """
    close = pd.Series(values)
    return  close.ewm(span=slow, adjust=False,min_periods=slow).mean()

def MACD_Backtesting_MACD(fast, slow):
    """
    Return exponential moving average of `values`, at
    each step taking into account `n` previous values.
    """
    return fast - slow

def MACD_Backtesting_SIGNAL(values, signal):
    """
    Return exponential moving average of `values`, at
    each step taking into account `n` previous values.
    """
    macd_ = pd.Series(values)
    return macd_.ewm(span=signal, adjust=False,min_periods=signal).mean()


def MACD_Backtesting_HISTOGRAMM(macd_, signal):
    """
    Return exponential moving average of `values`, at
    each step taking into account `n` previous values.
    """
    return macd_ - signal

class MACDStrategy(Strategy):
    
    # Define the two EMA lags as *class variables*
    # for later optimization
    fast = 12
    slow = 26
    signal = 9
    
    def init(self):                
        self.fast_macd      = self.I(MACD_Backtesting_FAST, self.data.Close, self.fast, overlay=True, name='FAST')
        self.slow_macd      = self.I(MACD_Backtesting_SLOW, self.data.Close, self.slow, overlay=True, name='SLOW')        
        self.macd_macd_      = MACD_Backtesting_MACD(self.fast_macd, self.slow_macd)        
        self.signal_macd_    = MACD_Backtesting_SIGNAL(self.macd_macd_, self.signal)
        self.histogram_macd_ = MACD_Backtesting_HISTOGRAMM(self.macd_macd_, self.signal_macd_)
        
        self.macd_macd, self.signal_macd, self.histogram_macd = self.I(
            lambda: (self.macd_macd_, self.signal_macd_, self.histogram_macd_)                                    
                , overlay=False,legends=['MACD', 'signal', 'histogramm'], histogramms=[False, False, True], scatter=False, name='MACD',)
        
        

vladimircape avatar Nov 16 '21 12:11 vladimircape

In backtesting.py

   def I(self,  # noqa: E741, E743
          func: Callable, *args,
          name=None, plot=True, overlay=None, color=None, scatter=False, histogram=False,legends=None,histogramms=None,
          **kwargs) -> np.ndarray:
        """
        Declare indicator. An indicator is just an array of values,
        but one that is revealed gradually in
        `backtesting.backtesting.Strategy.next` much like
        `backtesting.backtesting.Strategy.data` is.
        Returns `np.ndarray` of indicator values.

        `func` is a function that returns the indicator array(s) of
        same length as `backtesting.backtesting.Strategy.data`.

        In the plot legend, the indicator is labeled with
        function name, unless `name` overrides it.

        If `plot` is `True`, the indicator is plotted on the resulting
        `backtesting.backtesting.Backtest.plot`.

        If `overlay` is `True`, the indicator is plotted overlaying the
        price candlestick chart (suitable e.g. for moving averages).
        If `False`, the indicator is plotted standalone below the
        candlestick chart. By default, a heuristic is used which decides
        correctly most of the time.

        `color` can be string hex RGB triplet or X11 color name.
        By default, the next available color is assigned.

        If `scatter` is `True`, the plotted indicator marker will be a
        circle instead of a connected line segment (default).

        If `histogram` is `True`, the indicator values will be plotted
        as a histogram instead of line or circle. When `histogram` is
        `True`, 'scatter' value will be ignored even if it's set.

        Additional `*args` and `**kwargs` are passed to `func` and can
        be used for parameters.

        For example, using simple moving average function from TA-Lib:

            def init():
                self.sma = self.I(ta.SMA, self.data.Close, self.n_sma)
                
         `legends` can be list or array of string values to represent
        legends on your indicator chart. By default it's set to None,
        and `name` is used as legends.
        """
        if name is None:
            params = ','.join(filter(None, map(_as_str, chain(args, kwargs.values()))))
            func_name = _as_str(func)
            name = (f'{func_name}({params})' if params else f'{func_name}')
        else:
            name = name.format(*map(_as_str, args),
                               **dict(zip(kwargs.keys(), map(_as_str, kwargs.values()))))

        try:
            value = func(*args, **kwargs)
        except Exception as e:
            raise RuntimeError(f'Indicator "{name}" errored with exception: {e}')

        if isinstance(value, pd.DataFrame):
            value = value.values.T

        if value is not None:
            value = try_(lambda: np.asarray(value, order='C'), None)
        is_arraylike = value is not None

        # Optionally flip the array if the user returned e.g. `df.values`
        if is_arraylike and np.argmax(value.shape) == 0:
            value = value.T

        if not is_arraylike or not 1 <= value.ndim <= 2 or value.shape[-1] != len(self._data.Close):
            raise ValueError(
                'Indicators must return (optionally a tuple of) numpy.arrays of same '
                f'length as `data` (data shape: {self._data.Close.shape}; indicator "{name}"'
                f'shape: {getattr(value, "shape" , "")}, returned value: {value})')

        if plot and overlay is None and np.issubdtype(value.dtype, np.number):
            x = value / self._data.Close
            # By default, overlay if strong majority of indicator values
            # is within 30% of Close
            with np.errstate(invalid='ignore'):
                overlay = ((x < 1.4) & (x > .6)).mean() > .6

        value = _Indicator(value, name=name, plot=plot, overlay=overlay,
                           color=color, scatter=scatter, legends=legends, histogram=histogram, histogramms=histogramms,
                           # _Indicator.s Series accessor uses this:
                           index=self.data.index)
        self._indicators.append(value)
        return value

vladimircape avatar Nov 16 '21 12:11 vladimircape

_plotting.py

   def _plot_indicators():
        """Strategy indicators"""

        def _too_many_dims(value):
            assert value.ndim >= 2
            if value.ndim > 2:
                warnings.warn(f"Can't plot indicators with >2D ('{value.name}')",
                              stacklevel=5)
                return True
            return False

        class LegendStr(str):
            # The legend string is such a string that only matches
            # itself if it's the exact same object. This ensures
            # legend items are listed separately even when they have the
            # same string contents. Otherwise, Bokeh would always consider
            # equal strings as one and the same legend item.
            def __eq__(self, other):
                return self is other

        ohlc_colors = colorgen()
        indicator_figs = []

        for i, value in enumerate(indicators):
            value = np.atleast_2d(value)

            # Use .get()! A user might have assigned a Strategy.data-evolved
            # _Array without Strategy.I()
            if not value._opts.get('plot') or _too_many_dims(value):
                continue

            is_overlay = value._opts['overlay']
            is_scatter = value._opts['scatter']
            is_histogram = value._opts['histogram']
            if is_overlay:
                fig = fig_ohlc
            else:
                fig = new_indicator_figure()
                indicator_figs.append(fig)
            tooltips = []
            colors = value._opts['color']
            colors = colors and cycle(_as_list(colors)) or (
                cycle([next(ohlc_colors)]) if is_overlay else colorgen())
            legends = value._opts['legends']
            legends = legends and cycle(_as_list(legends))
            indicator_name = value.name
            legend_label = LegendStr(indicator_name)
            #Histogramm
            histogramms = value._opts['histogramms']
            histogramms = histogramms and cycle(_as_list(histogramms))
            
            for j, arr in enumerate(value, 1):
                color = next(colors)
                legend_label = next(legends) if legends is not None else legend_label
                is_histogram = next(histogramms) if histogramms is not None else is_histogram
                source_name = f'{indicator_name}_{i}_{j}'
                if arr.dtype == bool:
                    arr = arr.astype(int)
                source.add(arr, source_name)
                tooltips.append(f'@{{{source_name}}}{{0,0.0[0000]}}')
                if is_overlay:
                    ohlc_extreme_values[source_name] = arr
                    if is_histogram:
                        fig.vbar('index', BAR_WIDTH, source_name, source=source,
                                 legend_label=legend_label, color=color)
                    elif is_scatter:
                        fig.scatter(
                            'index', source_name, source=source,
                            legend_label=legend_label, color=color,
                            line_color='black', fill_alpha=.8,
                            marker='circle', radius=BAR_WIDTH / 2 * 1.5)
                    else:
                        fig.line(
                            'index', source_name, source=source,
                            legend_label=legend_label, line_color=color,
                            line_width=1.3)
                else:
                    if is_histogram:
                        r = fig.vbar('index', BAR_WIDTH, source_name, source=source,
                                     legend_label=LegendStr(legend_label), color=color)
                    elif is_scatter:
                        r = fig.scatter(
                            'index', source_name, source=source,
                            legend_label=LegendStr(legend_label), color=color,
                            marker='circle', radius=BAR_WIDTH / 2 * .9)
                    else:
                        r = fig.line(
                            'index', source_name, source=source,
                            legend_label=LegendStr(legend_label), line_color=color,
                            line_width=1.3)
                    # Add dashed centerline just because
                    mean = float(pd.Series(arr).mean())
                    if not np.isnan(mean) and (abs(mean) < .1 or
                                               round(abs(mean), 1) == .5 or
                                               round(abs(mean), -1) in (50, 100, 200)):
                        fig.add_layout(Span(location=float(mean), dimension='width',
                                            line_color='#666666', line_dash='dashed',
                                            line_width=.5))
            if is_overlay:
                ohlc_tooltips.append((indicator_name, NBSP.join(tooltips)))
            else:
                set_tooltips(fig, [(indicator_name, NBSP.join(tooltips))],
                             vline=True, renderers=[r])
                # If the sole indicator line on this figure,
                # have the legend only contain text without the glyph
                if len(value) == 1:
                    fig.legend.glyph_width = 0
        return indicator_figs

vladimircape avatar Nov 16 '21 12:11 vladimircape

I ended up implement everything my own. Thanks @vladimircape

wakandan avatar Nov 18 '21 08:11 wakandan

Hi @vladimircape,

Your tutorial is great. I did it but still cannot generate a histogram with different colors for positive and negative ones.

For now, the histogram has only one color:

image

Do you know how to generate a histogram with multiple colors as @AGG2017 did? Thank you.

quanengineering avatar May 08 '22 10:05 quanengineering