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 vbt.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)
- Values of an enum in vectorbt usually start with 0 and increase incrementally
- 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
- Using GenericAccessor.stats
The same can be done manually:
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:
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:
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:
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:
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
- 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
- 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)
- Index of the first data point
- Index of the last data point
- The number of data points multiplied by the index frequency
- 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. Thedicttype instructs vectorbt to make multiple metrics out of one. - The number of entry signals
- 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. - 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
figcontains the figure andadd_trace_kwargscontains 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.- 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:
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()
- Since both plotting functions can now be invoked by the user, we need to return the figure in case it wasn't provided
- We need to use Config instead of
dictwhen settingmetricsandsubplotsmanually - Since both functions have become attributes of the class, we can specify them as strings to make use of vectorbt's attribute resolution