Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Histogram indicator plot style #195

Open
unreal639 opened this issue Dec 13, 2020 · 9 comments · May be fixed by #384
Open

Histogram indicator plot style #195

unreal639 opened this issue Dec 13, 2020 · 9 comments · May be fixed by #384
Labels
enhancement New feature or request

Comments

@unreal639
Copy link

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

@kernc kernc changed the title MACD indicator plotting style Histogram indicator plot style Dec 13, 2020
@kernc kernc added the enhancement New feature or request label Dec 13, 2020
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
Add Histogram indicator plot style kernc#195
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
 Fix legends values on histogram kernc#195
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 15, 2021
zlpatel added a commit to zlpatel/backtesting.py that referenced this issue Jun 16, 2021
@AGG2017
Copy link

AGG2017 commented Aug 13, 2021

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

@kernc kernc linked a pull request Sep 22, 2021 that will close this issue
@wakandan
Copy link

@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.

@ghost
Copy link

ghost commented Nov 16, 2021

@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

@ghost
Copy link

ghost commented Nov 16, 2021

@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',)
        
        

@ghost
Copy link

ghost commented Nov 16, 2021

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

@ghost
Copy link

ghost commented Nov 16, 2021

_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

@wakandan
Copy link

I ended up implement everything my own. Thanks @vladimircape

@quantrung9
Copy link

quantrung9 commented May 8, 2022

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.

@kxbin
Copy link

kxbin commented Dec 3, 2024

I did it successfully, thanks to everyone under this issue for their help.
image

@quanvnd9 As for the histogram color, you can use:

cm = linear_cmap(source_name, [lightness(BEAR_COLOR, .6), lightness(BULL_COLOR, .6)], low=0, high=0)
r = fig.vbar('index', BAR_WIDTH, source_name, source=source, legend_label=LegendStr(legend_label), color=cm)

For details, please refer to my fork repository https://github.com/kxbin/backtesting.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants