Skip to content
New: Star-import, signal unraveling, Python 3.12 support, and more

#
Analysis

To analyze an indicator, we can use the indicator instance returned by the `run` method.

## Helper methods

Whenever we create an instance of IndicatorFactory, it builds and sets up an indicator class. During this setup, the factory attaches many interesting attributes to the class. For instance, for each in `input_names`, `in_output_names`, `output_names`, and `lazy_outputs`, it will create and attach a bunch of comparison and combination methods. What characteristics any of these attributes should have can be regulated in the `attr_settings` dictionary.

Let's modify the class `CrossSig` created earlier by replacing the entries and exits with a single signal array and also returning an enumerated array that contains the signal type. We will also specify the data type of each array in the `attr_settings` dictionary:

    >>> from vectorbtpro import *

    >>> SignalType = namedtuple('SigType', ['Entry', 'Exit'])(0, 1)  # (1)!

    >>> def apply_func(ts, fastw, sloww, minp=None):
    ...     fast_ma = vbt.nb.rolling_mean_nb(ts, fastw, minp=minp)
    ...     slow_ma = vbt.nb.rolling_mean_nb(ts, sloww, minp=minp)
    ...     entries = vbt.nb.crossed_above_nb(fast_ma, slow_ma)
    ...     exits = vbt.nb.crossed_above_nb(slow_ma, fast_ma)
    ...     signals = entries | exits
    ...     signal_type = np.full(ts.shape, -1, dtype=np.int_)  # (2)!
    ...     signal_type[entries] = SignalType.Entry
    ...     signal_type[exits] = SignalType.Exit
    ...     return (fast_ma, slow_ma, signals, signal_type)

    >>> CrossSig = vbt.IF(
    ...     class_name="CrossSig",
    ...     input_names=['ts'],
    ...     param_names=['fastw', 'sloww'],
    ...     output_names=['fast_ma', 'slow_ma', 'signals', 'signal_type'],
    ...     attr_settings=dict(
    ...         fast_ma=dict(dtype=np.float_),
    ...         slow_ma=dict(dtype=np.float_),
    ...         signals=dict(dtype=np.bool_),
    ...         signal_type=dict(dtype=SignalType),
    ...     )
    ... ).with_apply_func(apply_func)

    >>> def generate_index(n):
    ...     return pd.date_range("2020-01-01", periods=n)

    >>> ts = pd.DataFrame({
    ...     'a': [1, 2, 3, 2, 1, 2, 3],
    ...     'b': [3, 2, 1, 2, 3, 2, 1]
    ... }, index=generate_index(7))
    >>> cross_sig = CrossSig.run(ts, 2, 3)

1. Values of an enum in vectorbt usually start with 0 and increase incrementally
2. A missing value in vectorbt is usually represented with `-1`

We can explore the helper methods that were attached using the Python's `dir` command:

    >>> dir(cross_sig)
    ...
    'fast_ma',
    'fast_ma_above',
    'fast_ma_below',
    'fast_ma_crossed_above',
    'fast_ma_crossed_below',
    'fast_ma_equal',
    'fast_ma_stats',
    ...
    'signal_type',
    'signal_type_readable',
    'signal_type_stats',
    ...
    'signals',
    'signals_and',
    'signals_or',
    'signals_stats',
    'signals_xor',
    ...
    'slow_ma',
    'slow_ma_above',
    'slow_ma_below',
    'slow_ma_crossed_above',
    'slow_ma_crossed_below',
    'slow_ma_equal',
    'slow_ma_stats',
    ...
    'ts',
    'ts_above',
    'ts_below',
    'ts_crossed_above',
    'ts_crossed_below',
    'ts_equal',
    'ts_stats',

One helper method that appears for each array is `stats`, which calls StatsBuilderMixin.stats on the accessor that corresponds to the data type of the array:

    >>> cross_sig.fast_ma_stats(column=(2, 3, 'a'))  # (1)!
    Start        2020-01-01 00:00:00
    End          2020-01-07 00:00:00
    Period           7 days 00:00:00
    Count                          6
    Mean                         2.0
    Std                     0.547723
    Min                          1.5
    Median                       2.0
    Max                          2.5
    Min Index    2020-01-02 00:00:00
    Max Index    2020-01-03 00:00:00
    Name: (2, 3, a), dtype: object

1. Using GenericAccessor.stats

The same can be done manually:

    >>> cross_sig.fast_ma.vbt.stats(column=(2, 3, 'a'))

### Numeric

The factory generated the comparison methods `above`, `below`, and `equal` for the numeric arrays. Each of those methods is based on combine_objs, which in turn is based on BaseAccessor.combine. All operations are done strictly using NumPy. Another advantage is utilization of vectorbt's own broadcasting, such that we can combine the arrays with an arbitrary array-like object, given their shapes can broadcast together. We can also do comparison with multiple objects at once by passing them as a tuple or list.

Let's return True when the fast moving average is above a range of thresholds:

    >>> cross_sig.fast_ma_above([2, 3])
    crosssig_fast_ma_above             2             3
    crosssig_fastw                     2             2
    crosssig_sloww                     3             3
                                a      b      a      b
    2020-01-01              False  False  False  False
    2020-01-02              False   True  False  False
    2020-01-03               True  False  False  False
    2020-01-04               True  False  False  False
    2020-01-05              False   True  False  False
    2020-01-06              False   True  False  False
    2020-01-07               True  False  False  False

Or, manually:

    >>> cross_sig.fast_ma.vbt > vbt.Param([2, 3], name='crosssig_fast_ma_above')

Additionally, the factory attached the methods `crossed_above` and `crossed_below`, which are based on GenericAccessor.crossed_above and GenericAccessor.crossed_below respectively.

    >>> cross_sig.fast_ma_crossed_above(cross_sig.slow_ma)
    crosssig_fastw             2
    crosssig_sloww             3
                        a      b
    2020-01-01      False  False
    2020-01-02      False  False
    2020-01-03      False  False
    2020-01-04      False  False
    2020-01-05      False   True
    2020-01-06      False  False
    2020-01-07       True  False

Or, manually:

    >>> cross_sig.fast_ma.vbt.crossed_above(cross_sig.slow_ma)

### Boolean

The factory generated the comparison methods `and`, `or`, and `xor` for the boolean arrays. Similarly to the methods generated for the numeric arrays, they are also based on combine_objs:

    >>> other_signals = pd.Series([False, False, False, False, True, False, False])
    >>> cross_sig.signals_and(other_signals)
    crosssig_fastw             2
    crosssig_sloww             3
                        a      b
    2020-01-01      False  False
    2020-01-02      False  False
    2020-01-03      False  False
    2020-01-04      False  False
    2020-01-05       True   True
    2020-01-06      False  False
    2020-01-07      False  False

Or, manually:

    >>> cross_sig.signals.vbt & other_signals

### Enumerated

Enumerated (or categorical) arrays, such as our `signal_type`, contain integer data that can be mapped to a certain category using a named tuple or any other enum. In contrast to the numeric and boolean arrays, comparing them with other arrays would make no sense. Thus, there is only one attached method - `readable` - that allows us to print the array in a human-readable format:

    >>> cross_sig.signal_type_readable
    crosssig_fastw             2
    crosssig_sloww             3
                        a      b
    2020-01-01       None   None
    2020-01-02       None   None
    2020-01-03       None   None
    2020-01-04       None   None
    2020-01-05       Exit  Entry
    2020-01-06       None   None
    2020-01-07      Entry   Exit

Hint

In vectorbt, if `-1` is not listed in the enum, it automatically means a missing value and gets replaced by `None`.

Or, manually:

    >>> cross_sig.signal_type.vbt(mapping=SignalType).apply_mapping()

## Indexing

Each indicator class subclasses Analyzable, so we can perform Pandas indexing on the indicator instance to select rows and columns in all Pandas objects. Supported operations are `iloc`, `loc`, `xs`, and `__getitem__`.

    >>> cross_sig = CrossSig.run(ts, [2, 3], [3, 4], param_product=True)

    >>> cross_sig.loc["2020-01-03":, (2, 3, 'a')]  # (1)!
    <vectorbtpro.indicators.factory.CrossSig at 0x7fcaf8e2f5c0>

    >>> cross_sig.loc["2020-01-03":, (2, 3, 'a')].signals
    2020-01-03    False
    2020-01-04    False
    2020-01-05     True
    2020-01-06    False
    2020-01-07     True
    Name: (2, 3, a), dtype: bool

1. Using one Pandas indexing opreation

Additionally, IndicatorFactory uses the class factory function build_param_indexer to create an indexing class that enables Pandas indexing on each defined parameter. Since the indicator class subclasses this indexing class, we can use `*param_name*_loc` to select one or more values of any parameter.

    >>> cross_sig.fastw_loc[2].sloww_loc[3]['a']  # (1)!
    <vectorbtpro.indicators.factory.CrossSig at 0x7fcac80742b0>

    >>> cross_sig.fastw_loc[2].sloww_loc[3]['a'].signals
    2020-01-01    False
    2020-01-02    False
    2020-01-03    False
    2020-01-04    False
    2020-01-05     True
    2020-01-06    False
    2020-01-07     True
    Name: a, dtype: bool

1. Using two parameter and one Pandas indexing operation. Each of those operations returns a separate instance of `CrossSig`.

This all makes possible accessing rows and columns by labels, integer positions, and parameters - full flexibility

## Stats and plots

As with every Analyzable instance, we can compute and plot various properties of the (input and output) data stored in the instance.

Metrics can be defined in two ways: by passing them via the `metrics` argument, or by subclassing the indicator class. The same applies to the `stats_defaults` argument, which can be provided either as a dictionary or a function, and which defines the default settings for StatsBuilderMixin.stats. Subplots can be defined in a similar way to metrics, except that they are set up by `subplots` and `plots_defaults` arguments, and invoked by PlotsBuilderMixin.plots.

Let's define some metrics and subplots for `CrossSig`:

    >>> metrics = dict(
    ...     start=dict(  # (1)!
    ...         title='Start',
    ...         calc_func=lambda self: self.wrapper.index[0],
    ...         agg_func=None
    ...     ),
    ...     end=dict(  # (2)!
    ...         title='End',
    ...         calc_func=lambda self: self.wrapper.index[-1],
    ...         agg_func=None
    ...     ),
    ...     period=dict(  # (3)!
    ...         title='Period',
    ...         calc_func=lambda self: len(self.wrapper.index),
    ...         apply_to_timedelta=True,
    ...         agg_func=None
    ...     ),
    ...     fast_stats=dict(  # (4)!
    ...         title="Fast Stats",
    ...         calc_func=lambda self:
    ...         self.fast_ma.describe()\
    ...         .loc[['count', 'mean', 'std', 'min', 'max']]\
    ...         .vbt.to_dict(orient='index_series')
    ...     ),
    ...     slow_stats=dict(
    ...         title="Slow Stats",
    ...         calc_func=lambda self:
    ...         self.slow_ma.describe()\
    ...         .loc[['count', 'mean', 'std', 'min', 'max']]\
    ...         .vbt.to_dict(orient='index_series')
    ...     ),
    ...     num_entries=dict(  # (5)!
    ...         title="Entries",
    ...         calc_func=lambda self:
    ...         np.sum(self.signal_type == SignalType.Entry)
    ...     ),
    ...     num_exits=dict(
    ...         title="Exits",
    ...         calc_func=lambda self:
    ...         np.sum(self.signal_type == SignalType.Exit)
    ...     )
    ... )

    >>> def plot_mas(self, column=None, add_trace_kwargs=None, fig=None):  # (6)!
    ...     ts = self.select_col_from_obj(self.ts, column).rename('TS')  # (7)!
    ...     fast_ma = self.select_col_from_obj(self.fast_ma, column).rename('Fast MA')
    ...     slow_ma = self.select_col_from_obj(self.slow_ma, column).rename('Slow MA')
    ...     ts.vbt.plot(add_trace_kwargs=add_trace_kwargs, fig=fig)
    ...     fast_ma.vbt.plot(add_trace_kwargs=add_trace_kwargs, fig=fig)  # (8)!
    ...     slow_ma.vbt.plot(add_trace_kwargs=add_trace_kwargs, fig=fig)

    >>> def plot_signals(self, column=None, add_trace_kwargs=None, fig=None):
    ...     signal_type = self.select_col_from_obj(self.signal_type, column)
    ...     entries = (signal_type == SignalType.Entry).rename('Entries')
    ...     exits = (signal_type == SignalType.Exit).rename('Exits')
    ...     entries.vbt.plot(add_trace_kwargs=add_trace_kwargs, fig=fig)
    ...     exits.vbt.plot(add_trace_kwargs=add_trace_kwargs, fig=fig)

    >>> subplots = dict(
    ...     mas=dict(
    ...         title="Moving averages",
    ...         plot_func=plot_mas
    ...     ),
    ...     signals=dict(
    ...         title="Signals",
    ...         plot_func=plot_signals
    ...     )
    ... )

    >>> CrossSig = vbt.IF(
    ...     class_name="CrossSig",
    ...     input_names=['ts'],
    ...     param_names=['fastw', 'sloww'],
    ...     output_names=['fast_ma', 'slow_ma', 'signals', 'signal_type'],
    ...     attr_settings=dict(
    ...         fast_ma=dict(dtype=np.float_),
    ...         slow_ma=dict(dtype=np.float_),
    ...         signals=dict(dtype=np.bool_),
    ...         signal_type=dict(dtype=SignalType),
    ...     ),
    ...     metrics=metrics,  # (9)!
    ...     subplots=subplots
    ... ).with_apply_func(apply_func)

    >>> cross_sig = CrossSig.run(ts, [2, 3], 4)

1. Index of the first data point
2. Index of the last data point
3. The number of data points multiplied by the index frequency
4. Takes `fast_ma`, generates the descriptive statistics using Pandas, and converts them to a dictionary with Series (of columns) per statistic using BaseAccessor.to_dict. The `dict` type instructs vectorbt to make multiple metrics out of one.
5. The number of entry signals
6. PlotsBuilderMixin.plots can automatically recognize the arguments the plotting function takes, and pass the necessary information. For example, by seeing `self`, it will pass the indicator instance.
7. Since we are already plotting multiple time series, we cannot plot multiple columns at once, so we must select the provided column from each time series using Wrapping.select_col_from_obj
8. `fig` contains the figure and `add_trace_kwargs` contains the specification for the subplot this time series should be plotted over. Note how the function returns nothing: the figure is being modified in-place.
9. Metric and subplot dictionaries are passed to the constructor

Calculate the metrics:

    >>> cross_sig.stats(column=(2, 4, 'a'))
    Start                2020-01-01 00:00:00
    End                  2020-01-07 00:00:00
    Period                   7 days 00:00:00
    Fast Stats: count                    6.0
    Fast Stats: mean                     2.0
    Fast Stats: std                 0.547723
    Fast Stats: min                      1.5
    Fast Stats: max                      2.5
    Slow Stats: count                    4.0
    Slow Stats: mean                     2.0
    Slow Stats: std                      0.0
    Slow Stats: min                      2.0
    Slow Stats: max                      2.0
    Entries                                1
    Exits                                  1
    Name: (2, 4, a), dtype: object

Plot the subplots:

    >>> cross_sig.plots(column=(2, 4, 'a')).show()

We have created a smart indicator, yay!

## Extending

Indicator classes can be extended and modified just as regular Python classes - by subclassing. Let's make the newly created functions `plot_mas` and `plot_signals` methods of the indicator class, such that we can plot both graphs separately. We will also redefine the `subplots` config to account for this change:

    >>> class SmartCrossSig(CrossSig):
    ...     def plot_mas(self, column=None, add_trace_kwargs=None, fig=None):
    ...         ts = self.select_col_from_obj(self.ts, column).rename('TS')
    ...         fast_ma = self.select_col_from_obj(self.fast_ma, column).rename('Fast MA')
    ...         slow_ma = self.select_col_from_obj(self.slow_ma, column).rename('Slow MA')
    ...         fig = ts.vbt.plot(add_trace_kwargs=add_trace_kwargs, fig=fig)
    ...         fast_ma.vbt.plot(add_trace_kwargs=add_trace_kwargs, fig=fig)
    ...         slow_ma.vbt.plot(add_trace_kwargs=add_trace_kwargs, fig=fig)
    ...         return fig  # (1)!
    ...
    ...     def plot_signals(self, column=None, add_trace_kwargs=None, fig=None):
    ...         signal_type = self.select_col_from_obj(self.signal_type, column)
    ...         entries = (signal_type == SignalType.Entry).rename('Entries')
    ...         exits = (signal_type == SignalType.Exit).rename('Exits')
    ...         fig = entries.vbt.plot(add_trace_kwargs=add_trace_kwargs, fig=fig)
    ...         exits.vbt.plot(add_trace_kwargs=add_trace_kwargs, fig=fig)
    ...         return fig
    ...
    ...     subplots = vbt.HybridConfig(  # (2)!
    ...         mas=dict(
    ...             title="Moving averages",
    ...             plot_func='plot_mas'  # (3)!
    ...         ),
    ...         signals=dict(
    ...             title="Signals",
    ...             plot_func='plot_signals'
    ...         )
    ...     )

    >>> smart_cross_sig = SmartCrossSig.run(ts, [2, 3], 4)
    >>> smart_cross_sig.plot_signals(column=(2, 4, 'a')).show()

1. Since both plotting functions can now be invoked by the user, we need to return the figure in case it wasn't provided
2. We need to use Config instead of `dict` when setting `metrics` and `subplots` manually
3. Since both functions have become attributes of the class, we can specify them as strings to make use of vectorbt's attribute resolution

Python code

#### Cookie consent

We use cookies to recognize your repeated visits and preferences, as well as to measure the effectiveness of our documentation and whether users find what they're searching for. With your consent, you're helping us to make our documentation better.

- Google Analytics
- GitHub

Manage settings