Example #1
0
    def __init__(self, obj: tp.SeriesFrame, wrapper: tp.Optional[ArrayWrapper] = None, **kwargs) -> None:
        checks.assert_instance_of(obj, (pd.Series, pd.DataFrame))

        self._obj = obj

        wrapper_arg_names = get_func_arg_names(ArrayWrapper.__init__)
        grouper_arg_names = get_func_arg_names(ColumnGrouper.__init__)
        wrapping_kwargs = dict()
        for k in list(kwargs.keys()):
            if k in wrapper_arg_names or k in grouper_arg_names:
                wrapping_kwargs[k] = kwargs.pop(k)
        if wrapper is None:
            wrapper = ArrayWrapper.from_obj(obj, **wrapping_kwargs)
        else:
            wrapper = wrapper.replace(**wrapping_kwargs)
        Wrapping.__init__(self, wrapper, obj=obj, **kwargs)
Example #2
0
    def call(self, mapping: tp.Optional[tp.Mapping] = None) -> tp.Any:
        """Call `RepFunc.func` using `mapping`.

        Merges `mapping` and `RepFunc.mapping`."""
        mapping = merge_dicts(self.mapping, mapping)
        func_arg_names = get_func_arg_names(self.func)
        func_kwargs = dict()
        for k, v in mapping.items():
            if k in func_arg_names:
                func_kwargs[k] = v
        return self.func(**func_kwargs)
Example #3
0
 def new_method(self,
                _target_name: str = target_name,
                _transformer: tp.Union[tp.Type[TransformerT],
                                       TransformerT] = transformer,
                **kwargs) -> tp.SeriesFrame:
     if inspect.isclass(_transformer):
         arg_names = get_func_arg_names(_transformer.__init__)
         transformer_kwargs = dict()
         for arg_name in arg_names:
             if arg_name in kwargs:
                 transformer_kwargs[arg_name] = kwargs.pop(arg_name)
         return self.transform(_transformer(**transformer_kwargs),
                               **kwargs)
     return self.transform(_transformer, **kwargs)
Example #4
0
                def new_method(self,
                               *,
                               _func: tp.Callable = qs_func,
                               **kwargs) -> tp.Any:
                    returns = self.returns_accessor.obj
                    if isinstance(returns, pd.DataFrame):
                        null_mask = returns.isnull().any(axis=1)
                    else:
                        null_mask = returns.isnull()
                    func_arg_names = get_func_arg_names(_func)
                    defaults = self.defaults

                    pass_kwargs = dict()
                    for arg_name in func_arg_names:
                        if arg_name not in kwargs:
                            if arg_name in defaults:
                                pass_kwargs[arg_name] = defaults[arg_name]
                            elif arg_name == 'benchmark':
                                if self.returns_accessor.benchmark_rets is not None:
                                    pass_kwargs[
                                        'benchmark'] = self.returns_accessor.benchmark_rets
                            elif arg_name == 'periods':
                                pass_kwargs['periods'] = int(
                                    self.returns_accessor.ann_factor)
                            elif arg_name == 'periods_per_year':
                                pass_kwargs['periods_per_year'] = int(
                                    self.returns_accessor.ann_factor)
                        else:
                            pass_kwargs[arg_name] = kwargs[arg_name]

                    if 'benchmark' in pass_kwargs:
                        if isinstance(pass_kwargs['benchmark'], pd.DataFrame):
                            bm_null_mask = pass_kwargs['benchmark'].isnull(
                            ).any(axis=1)
                        else:
                            bm_null_mask = pass_kwargs['benchmark'].isnull()
                        null_mask = null_mask | bm_null_mask
                        pass_kwargs['benchmark'] = pass_kwargs[
                            'benchmark'].loc[~null_mask]
                    returns = returns.loc[~null_mask]

                    signature(_func).bind(returns=returns, **pass_kwargs)
                    return _func(returns=returns, **pass_kwargs)
Example #5
0
    def plots(self,
              subplots: tp.Optional[tp.MaybeIterable[tp.Union[str, tp.Tuple[
                  str, tp.Kwargs]]]] = None,
              tags: tp.Optional[tp.MaybeIterable[str]] = None,
              column: tp.Optional[tp.Label] = None,
              group_by: tp.GroupByLike = None,
              silence_warnings: tp.Optional[bool] = None,
              template_mapping: tp.Optional[tp.Mapping] = None,
              settings: tp.KwargsLike = None,
              filters: tp.KwargsLike = None,
              subplot_settings: tp.KwargsLike = None,
              show_titles: bool = None,
              hide_id_labels: bool = None,
              group_id_labels: bool = None,
              make_subplots_kwargs: tp.KwargsLike = None,
              **layout_kwargs) -> tp.Optional[tp.BaseFigure]:
        """Plot various parts of this object.

        Args:
            subplots (str, tuple, iterable, or dict): Subplots to plot.

                Each element can be either:

                * a subplot name (see keys in `PlotsBuilderMixin.subplots`)
                * a tuple of a subplot name and a settings dict as in `PlotsBuilderMixin.subplots`.

                The settings dict can contain the following keys:

                * `title`: Title of the subplot. Defaults to the name.
                * `plot_func` (required): Plotting function for custom subplots.
                    Should write the supplied figure `fig` in-place and can return anything (it won't be used).
                * `xaxis_kwargs`: Layout keyword arguments for the x-axis. Defaults to `dict(title='Index')`.
                * `yaxis_kwargs`: Layout keyword arguments for the y-axis. Defaults to empty dict.
                * `tags`, `check_{filter}`, `inv_check_{filter}`, `resolve_plot_func`, `pass_{arg}`,
                    `resolve_path_{arg}`, `resolve_{arg}` and `template_mapping`:
                    The same as in `vectorbt.generic.stats_builder.StatsBuilderMixin` for `calc_func`.
                * Any other keyword argument that overrides the settings or is passed directly to `plot_func`.

                If `resolve_plot_func` is True, the plotting function may "request" any of the
                following arguments by accepting them or if `pass_{arg}` was found in the settings dict:

                * Each of `vectorbt.utils.attr_.AttrResolver.self_aliases`: original object
                    (ungrouped, with no column selected)
                * `group_by`: won't be passed if it was used in resolving the first attribute of `plot_func`
                    specified as a path, use `pass_group_by=True` to pass anyway
                * `column`
                * `subplot_name`
                * `trace_names`: list with the subplot name, can't be used in templates
                * `add_trace_kwargs`: dict with subplot row and column index
                * `xref`
                * `yref`
                * `xaxis`
                * `yaxis`
                * `x_domain`
                * `y_domain`
                * `fig`
                * `silence_warnings`
                * Any argument from `settings`
                * Any attribute of this object if it meant to be resolved
                    (see `vectorbt.utils.attr_.AttrResolver.resolve_attr`)

                !!! note
                    Layout-related resolution arguments such as `add_trace_kwargs` are unavailable
                    before filtering and thus cannot be used in any templates but can still be overridden.

                Pass `subplots='all'` to plot all supported subplots.
            tags (str or iterable): See `tags` in `vectorbt.generic.stats_builder.StatsBuilderMixin`.
            column (str): See `column` in `vectorbt.generic.stats_builder.StatsBuilderMixin`.
            group_by (any): See `group_by` in `vectorbt.generic.stats_builder.StatsBuilderMixin`.
            silence_warnings (bool): See `silence_warnings` in `vectorbt.generic.stats_builder.StatsBuilderMixin`.
            template_mapping (mapping): See `template_mapping` in `vectorbt.generic.stats_builder.StatsBuilderMixin`.

                Applied on `settings`, `make_subplots_kwargs`, and `layout_kwargs`, and then on each subplot settings.
            filters (dict): See `filters` in `vectorbt.generic.stats_builder.StatsBuilderMixin`.
            settings (dict): See `settings` in `vectorbt.generic.stats_builder.StatsBuilderMixin`.
            subplot_settings (dict): See `metric_settings` in `vectorbt.generic.stats_builder.StatsBuilderMixin`.
            show_titles (bool): Whether to show the title of each subplot.
            hide_id_labels (bool): Whether to hide identical legend labels.

                Two labels are identical if their name, marker style and line style match.
            group_id_labels (bool): Whether to group identical legend labels.
            make_subplots_kwargs (dict): Keyword arguments passed to `plotly.subplots.make_subplots`.
            **layout_kwargs: Keyword arguments used to update the layout of the figure.

        !!! note
            `PlotsBuilderMixin` and `vectorbt.generic.stats_builder.StatsBuilderMixin` are very similar.
            Some artifacts follow the same concept, just named differently:

            * `plots_defaults` vs `stats_defaults`
            * `subplots` vs `metrics`
            * `subplot_settings` vs `metric_settings`

            See further notes under `vectorbt.generic.stats_builder.StatsBuilderMixin`.

        Usage:
            See `vectorbt.portfolio.base` for examples.
        """
        from vectorbt._settings import settings as _settings
        plotting_cfg = _settings['plotting']

        # Resolve defaults
        if silence_warnings is None:
            silence_warnings = self.plots_defaults['silence_warnings']
        if show_titles is None:
            show_titles = self.plots_defaults['show_titles']
        if hide_id_labels is None:
            hide_id_labels = self.plots_defaults['hide_id_labels']
        if group_id_labels is None:
            group_id_labels = self.plots_defaults['group_id_labels']
        template_mapping = merge_dicts(self.plots_defaults['template_mapping'],
                                       template_mapping)
        filters = merge_dicts(self.plots_defaults['filters'], filters)
        settings = merge_dicts(self.plots_defaults['settings'], settings)
        subplot_settings = merge_dicts(self.plots_defaults['subplot_settings'],
                                       subplot_settings)
        make_subplots_kwargs = merge_dicts(
            self.plots_defaults['make_subplots_kwargs'], make_subplots_kwargs)
        layout_kwargs = merge_dicts(self.plots_defaults['layout_kwargs'],
                                    layout_kwargs)

        # Replace templates globally (not used at subplot level)
        if len(template_mapping) > 0:
            sub_settings = deep_substitute(settings, mapping=template_mapping)
            sub_make_subplots_kwargs = deep_substitute(
                make_subplots_kwargs, mapping=template_mapping)
            sub_layout_kwargs = deep_substitute(layout_kwargs,
                                                mapping=template_mapping)
        else:
            sub_settings = settings
            sub_make_subplots_kwargs = make_subplots_kwargs
            sub_layout_kwargs = layout_kwargs

        # Resolve self
        reself = self.resolve_self(cond_kwargs=sub_settings,
                                   impacts_caching=False,
                                   silence_warnings=silence_warnings)

        # Prepare subplots
        if subplots is None:
            subplots = reself.plots_defaults['subplots']
        if subplots == 'all':
            subplots = reself.subplots
        if isinstance(subplots, dict):
            subplots = list(subplots.items())
        if isinstance(subplots, (str, tuple)):
            subplots = [subplots]

        # Prepare tags
        if tags is None:
            tags = reself.plots_defaults['tags']
        if isinstance(tags, str) and tags == 'all':
            tags = None
        if isinstance(tags, (str, tuple)):
            tags = [tags]

        # Bring to the same shape
        new_subplots = []
        for i, subplot in enumerate(subplots):
            if isinstance(subplot, str):
                subplot = (subplot, reself.subplots[subplot])
            if not isinstance(subplot, tuple):
                raise TypeError(
                    f"Subplot at index {i} must be either a string or a tuple")
            new_subplots.append(subplot)
        subplots = new_subplots

        # Handle duplicate names
        subplot_counts = Counter(list(map(lambda x: x[0], subplots)))
        subplot_i = {k: -1 for k in subplot_counts.keys()}
        subplots_dct = {}
        for i, (subplot_name, _subplot_settings) in enumerate(subplots):
            if subplot_counts[subplot_name] > 1:
                subplot_i[subplot_name] += 1
                subplot_name = subplot_name + '_' + str(
                    subplot_i[subplot_name])
            subplots_dct[subplot_name] = _subplot_settings

        # Check subplot_settings
        missed_keys = set(subplot_settings.keys()).difference(
            set(subplots_dct.keys()))
        if len(missed_keys) > 0:
            raise ValueError(
                f"Keys {missed_keys} in subplot_settings could not be matched with any subplot"
            )

        # Merge settings
        opt_arg_names_dct = {}
        custom_arg_names_dct = {}
        resolved_self_dct = {}
        mapping_dct = {}
        for subplot_name, _subplot_settings in list(subplots_dct.items()):
            opt_settings = merge_dicts(
                {name: reself
                 for name in reself.self_aliases},
                dict(column=column,
                     group_by=group_by,
                     subplot_name=subplot_name,
                     trace_names=[subplot_name],
                     silence_warnings=silence_warnings), settings)
            _subplot_settings = _subplot_settings.copy()
            passed_subplot_settings = subplot_settings.get(subplot_name, {})
            merged_settings = merge_dicts(opt_settings, _subplot_settings,
                                          passed_subplot_settings)
            subplot_template_mapping = merged_settings.pop(
                'template_mapping', {})
            template_mapping_merged = merge_dicts(template_mapping,
                                                  subplot_template_mapping)
            template_mapping_merged = deep_substitute(template_mapping_merged,
                                                      mapping=merged_settings)
            mapping = merge_dicts(template_mapping_merged, merged_settings)
            # safe because we will use deep_substitute again once layout params are known
            merged_settings = deep_substitute(merged_settings,
                                              mapping=mapping,
                                              safe=True)

            # Filter by tag
            if tags is not None:
                in_tags = merged_settings.get('tags', None)
                if in_tags is None or not match_tags(tags, in_tags):
                    subplots_dct.pop(subplot_name, None)
                    continue

            custom_arg_names = set(_subplot_settings.keys()).union(
                set(passed_subplot_settings.keys()))
            opt_arg_names = set(opt_settings.keys())
            custom_reself = reself.resolve_self(
                cond_kwargs=merged_settings,
                custom_arg_names=custom_arg_names,
                impacts_caching=True,
                silence_warnings=merged_settings['silence_warnings'])

            subplots_dct[subplot_name] = merged_settings
            custom_arg_names_dct[subplot_name] = custom_arg_names
            opt_arg_names_dct[subplot_name] = opt_arg_names
            resolved_self_dct[subplot_name] = custom_reself
            mapping_dct[subplot_name] = mapping

        # Filter subplots
        for subplot_name, _subplot_settings in list(subplots_dct.items()):
            custom_reself = resolved_self_dct[subplot_name]
            mapping = mapping_dct[subplot_name]
            _silence_warnings = _subplot_settings.get('silence_warnings')

            subplot_filters = set()
            for k in _subplot_settings.keys():
                filter_name = None
                if k.startswith('check_'):
                    filter_name = k[len('check_'):]
                elif k.startswith('inv_check_'):
                    filter_name = k[len('inv_check_'):]
                if filter_name is not None:
                    if filter_name not in filters:
                        raise ValueError(
                            f"Metric '{subplot_name}' requires filter '{filter_name}'"
                        )
                    subplot_filters.add(filter_name)

            for filter_name in subplot_filters:
                filter_settings = filters[filter_name]
                _filter_settings = deep_substitute(filter_settings,
                                                   mapping=mapping)
                filter_func = _filter_settings['filter_func']
                warning_message = _filter_settings.get('warning_message', None)
                inv_warning_message = _filter_settings.get(
                    'inv_warning_message', None)
                to_check = _subplot_settings.get('check_' + filter_name, False)
                inv_to_check = _subplot_settings.get(
                    'inv_check_' + filter_name, False)

                if to_check or inv_to_check:
                    whether_true = filter_func(custom_reself,
                                               _subplot_settings)
                    to_remove = (to_check
                                 and not whether_true) or (inv_to_check
                                                           and whether_true)
                    if to_remove:
                        if to_check and warning_message is not None and not _silence_warnings:
                            warnings.warn(warning_message)
                        if inv_to_check and inv_warning_message is not None and not _silence_warnings:
                            warnings.warn(inv_warning_message)

                        subplots_dct.pop(subplot_name, None)
                        custom_arg_names_dct.pop(subplot_name, None)
                        opt_arg_names_dct.pop(subplot_name, None)
                        resolved_self_dct.pop(subplot_name, None)
                        mapping_dct.pop(subplot_name, None)
                        break

        # Any subplots left?
        if len(subplots_dct) == 0:
            if not silence_warnings:
                warnings.warn("No subplots to plot", stacklevel=2)
            return None

        # Set up figure
        rows = sub_make_subplots_kwargs.pop('rows', len(subplots_dct))
        cols = sub_make_subplots_kwargs.pop('cols', 1)
        specs = sub_make_subplots_kwargs.pop('specs', [[{}
                                                        for _ in range(cols)]
                                                       for _ in range(rows)])
        row_col_tuples = []
        for row, row_spec in enumerate(specs):
            for col, col_spec in enumerate(row_spec):
                if col_spec is not None:
                    row_col_tuples.append((row + 1, col + 1))
        shared_xaxes = sub_make_subplots_kwargs.pop('shared_xaxes', True)
        shared_yaxes = sub_make_subplots_kwargs.pop('shared_yaxes', False)
        default_height = plotting_cfg['layout']['height']
        default_width = plotting_cfg['layout']['width'] + 50
        min_space = 10  # space between subplots with no axis sharing
        max_title_spacing = 30
        max_xaxis_spacing = 50
        max_yaxis_spacing = 100
        legend_height = 50
        if show_titles:
            title_spacing = max_title_spacing
        else:
            title_spacing = 0
        if not shared_xaxes and rows > 1:
            xaxis_spacing = max_xaxis_spacing
        else:
            xaxis_spacing = 0
        if not shared_yaxes and cols > 1:
            yaxis_spacing = max_yaxis_spacing
        else:
            yaxis_spacing = 0
        if 'height' in sub_layout_kwargs:
            height = sub_layout_kwargs.pop('height')
        else:
            height = default_height + title_spacing
            if rows > 1:
                height *= rows
                height += min_space * rows - min_space
                height += legend_height - legend_height * rows
                if shared_xaxes:
                    height += max_xaxis_spacing - max_xaxis_spacing * rows
        if 'width' in sub_layout_kwargs:
            width = sub_layout_kwargs.pop('width')
        else:
            width = default_width
            if cols > 1:
                width *= cols
                width += min_space * cols - min_space
                if shared_yaxes:
                    width += max_yaxis_spacing - max_yaxis_spacing * cols
        if height is not None:
            if 'vertical_spacing' in sub_make_subplots_kwargs:
                vertical_spacing = sub_make_subplots_kwargs.pop(
                    'vertical_spacing')
            else:
                vertical_spacing = min_space + title_spacing + xaxis_spacing
            if vertical_spacing is not None and vertical_spacing > 1:
                vertical_spacing /= height
            legend_y = 1 + (min_space + title_spacing) / height
        else:
            vertical_spacing = sub_make_subplots_kwargs.pop(
                'vertical_spacing', None)
            legend_y = 1.02
        if width is not None:
            if 'horizontal_spacing' in sub_make_subplots_kwargs:
                horizontal_spacing = sub_make_subplots_kwargs.pop(
                    'horizontal_spacing')
            else:
                horizontal_spacing = min_space + yaxis_spacing
            if horizontal_spacing is not None and horizontal_spacing > 1:
                horizontal_spacing /= width
        else:
            horizontal_spacing = sub_make_subplots_kwargs.pop(
                'horizontal_spacing', None)
        if show_titles:
            _subplot_titles = []
            for i in range(len(subplots_dct)):
                _subplot_titles.append('$title_' + str(i))
        else:
            _subplot_titles = None
        fig = make_subplots(rows=rows,
                            cols=cols,
                            specs=specs,
                            shared_xaxes=shared_xaxes,
                            shared_yaxes=shared_yaxes,
                            subplot_titles=_subplot_titles,
                            vertical_spacing=vertical_spacing,
                            horizontal_spacing=horizontal_spacing,
                            **sub_make_subplots_kwargs)
        sub_layout_kwargs = merge_dicts(
            dict(showlegend=True,
                 width=width,
                 height=height,
                 legend=dict(orientation="h",
                             yanchor="bottom",
                             y=legend_y,
                             xanchor="right",
                             x=1,
                             traceorder='normal')), sub_layout_kwargs)
        fig.update_layout(
            **sub_layout_kwargs)  # final destination for sub_layout_kwargs

        # Plot subplots
        arg_cache_dct = {}
        for i, (subplot_name,
                _subplot_settings) in enumerate(subplots_dct.items()):
            try:
                final_kwargs = _subplot_settings.copy()
                opt_arg_names = opt_arg_names_dct[subplot_name]
                custom_arg_names = custom_arg_names_dct[subplot_name]
                custom_reself = resolved_self_dct[subplot_name]
                mapping = mapping_dct[subplot_name]

                # Compute figure artifacts
                row, col = row_col_tuples[i]
                xref = 'x' if i == 0 else 'x' + str(i + 1)
                yref = 'y' if i == 0 else 'y' + str(i + 1)
                xaxis = 'xaxis' + xref[1:]
                yaxis = 'yaxis' + yref[1:]
                x_domain = get_domain(xref, fig)
                y_domain = get_domain(yref, fig)
                subplot_layout_kwargs = dict(
                    add_trace_kwargs=dict(row=row, col=col),
                    xref=xref,
                    yref=yref,
                    xaxis=xaxis,
                    yaxis=yaxis,
                    x_domain=x_domain,
                    y_domain=y_domain,
                    fig=fig,
                    pass_fig=True  # force passing fig
                )
                for k in subplot_layout_kwargs:
                    opt_arg_names.add(k)
                    if k in final_kwargs:
                        custom_arg_names.add(k)
                final_kwargs = merge_dicts(subplot_layout_kwargs, final_kwargs)
                mapping = merge_dicts(subplot_layout_kwargs, mapping)
                final_kwargs = deep_substitute(final_kwargs, mapping=mapping)

                # Clean up keys
                for k, v in list(final_kwargs.items()):
                    if k.startswith('check_') or k.startswith(
                            'inv_check_') or k in ('tags', ):
                        final_kwargs.pop(k, None)

                # Get subplot-specific values
                _column = final_kwargs.get('column')
                _group_by = final_kwargs.get('group_by')
                _silence_warnings = final_kwargs.get('silence_warnings')
                title = final_kwargs.pop('title', subplot_name)
                plot_func = final_kwargs.pop('plot_func', None)
                xaxis_kwargs = final_kwargs.pop('xaxis_kwargs', None)
                yaxis_kwargs = final_kwargs.pop('yaxis_kwargs', None)
                resolve_plot_func = final_kwargs.pop('resolve_plot_func', True)
                use_caching = final_kwargs.pop('use_caching', True)

                if plot_func is not None:
                    # Resolve plot_func
                    if resolve_plot_func:
                        if not callable(plot_func):
                            passed_kwargs_out = {}

                            def _getattr_func(
                                obj: tp.Any,
                                attr: str,
                                args: tp.ArgsLike = None,
                                kwargs: tp.KwargsLike = None,
                                call_attr: bool = True,
                                _final_kwargs: tp.Kwargs = final_kwargs,
                                _opt_arg_names: tp.Set[str] = opt_arg_names,
                                _custom_arg_names: tp.
                                Set[str] = custom_arg_names,
                                _arg_cache_dct: tp.Kwargs = arg_cache_dct
                            ) -> tp.Any:
                                if attr in final_kwargs:
                                    return final_kwargs[attr]
                                if args is None:
                                    args = ()
                                if kwargs is None:
                                    kwargs = {}
                                if obj is custom_reself and _final_kwargs.pop(
                                        'resolve_path_' + attr, True):
                                    if call_attr:
                                        return custom_reself.resolve_attr(
                                            attr,
                                            args=args,
                                            cond_kwargs={
                                                k: v
                                                for k, v in
                                                _final_kwargs.items()
                                                if k in _opt_arg_names
                                            },
                                            kwargs=kwargs,
                                            custom_arg_names=_custom_arg_names,
                                            cache_dct=_arg_cache_dct,
                                            use_caching=use_caching,
                                            passed_kwargs_out=passed_kwargs_out
                                        )
                                    return getattr(obj, attr)
                                out = getattr(obj, attr)
                                if callable(out) and call_attr:
                                    return out(*args, **kwargs)
                                return out

                            plot_func = custom_reself.deep_getattr(
                                plot_func,
                                getattr_func=_getattr_func,
                                call_last_attr=False)

                            if 'group_by' in passed_kwargs_out:
                                if 'pass_group_by' not in final_kwargs:
                                    final_kwargs.pop('group_by', None)
                        if not callable(plot_func):
                            raise TypeError("plot_func must be callable")

                        # Resolve arguments
                        func_arg_names = get_func_arg_names(plot_func)
                        for k in func_arg_names:
                            if k not in final_kwargs:
                                if final_kwargs.pop('resolve_' + k, False):
                                    try:
                                        arg_out = custom_reself.resolve_attr(
                                            k,
                                            cond_kwargs=final_kwargs,
                                            custom_arg_names=custom_arg_names,
                                            cache_dct=arg_cache_dct,
                                            use_caching=use_caching)
                                    except AttributeError:
                                        continue
                                    final_kwargs[k] = arg_out
                        for k in list(final_kwargs.keys()):
                            if k in opt_arg_names:
                                if 'pass_' + k in final_kwargs:
                                    if not final_kwargs.get(
                                            'pass_' + k):  # first priority
                                        final_kwargs.pop(k, None)
                                elif k not in func_arg_names:  # second priority
                                    final_kwargs.pop(k, None)
                        for k in list(final_kwargs.keys()):
                            if k.startswith('pass_') or k.startswith(
                                    'resolve_'):
                                final_kwargs.pop(k, None)  # cleanup

                        # Call plot_func
                        plot_func(**final_kwargs)
                    else:
                        # Do not resolve plot_func
                        plot_func(custom_reself, _subplot_settings)

                # Update global layout
                for annotation in fig.layout.annotations:
                    if 'text' in annotation and annotation[
                            'text'] == '$title_' + str(i):
                        annotation['text'] = title
                subplot_layout = dict()
                subplot_layout[xaxis] = merge_dicts(dict(title='Index'),
                                                    xaxis_kwargs)
                subplot_layout[yaxis] = merge_dicts(dict(), yaxis_kwargs)
                fig.update_layout(**subplot_layout)
            except Exception as e:
                warnings.warn(f"Subplot '{subplot_name}' raised an exception",
                              stacklevel=2)
                raise e

        # Remove duplicate legend labels
        found_ids = dict()
        unique_idx = 0
        for trace in fig.data:
            if 'name' in trace:
                name = trace['name']
            else:
                name = None
            if 'marker' in trace:
                marker = trace['marker']
            else:
                marker = {}
            if 'symbol' in marker:
                marker_symbol = marker['symbol']
            else:
                marker_symbol = None
            if 'color' in marker:
                marker_color = marker['color']
            else:
                marker_color = None
            if 'line' in trace:
                line = trace['line']
            else:
                line = {}
            if 'dash' in line:
                line_dash = line['dash']
            else:
                line_dash = None
            if 'color' in line:
                line_color = line['color']
            else:
                line_color = None

            id = (name, marker_symbol, marker_color, line_dash, line_color)
            if id in found_ids:
                if hide_id_labels:
                    trace['showlegend'] = False
                if group_id_labels:
                    trace['legendgroup'] = found_ids[id]
            else:
                if group_id_labels:
                    trace['legendgroup'] = unique_idx
                found_ids[id] = unique_idx
                unique_idx += 1

        # Remove all except the last title if sharing the same axis
        if shared_xaxes:
            i = 0
            for row in range(rows):
                for col in range(cols):
                    if specs[row][col] is not None:
                        xaxis = 'xaxis' if i == 0 else 'xaxis' + str(i + 1)
                        if row < rows - 1:
                            fig.layout[xaxis]['title'] = None
                        i += 1
        if shared_yaxes:
            i = 0
            for row in range(rows):
                for col in range(cols):
                    if specs[row][col] is not None:
                        yaxis = 'yaxis' if i == 0 else 'yaxis' + str(i + 1)
                        if col > 0:
                            fig.layout[yaxis]['title'] = None
                        i += 1

        # Return the figure
        return fig
Example #6
0
    def stats(
            self,
            metrics: tp.Optional[tp.MaybeIterable[tp.Union[str, tp.Tuple[
                str, tp.Kwargs]]]] = None,
            tags: tp.Optional[tp.MaybeIterable[str]] = None,
            column: tp.Optional[tp.Label] = None,
            group_by: tp.GroupByLike = None,
            agg_func: tp.Optional[tp.Callable] = np.mean,
            silence_warnings: tp.Optional[bool] = None,
            template_mapping: tp.Optional[tp.Mapping] = None,
            settings: tp.KwargsLike = None,
            filters: tp.KwargsLike = None,
            metric_settings: tp.KwargsLike = None
    ) -> tp.Optional[tp.SeriesFrame]:
        """Compute various metrics on this object.

        Args:
            metrics (str, tuple, iterable, or dict): Metrics to calculate.

                Each element can be either:

                * a metric name (see keys in `StatsBuilderMixin.metrics`)
                * a tuple of a metric name and a settings dict as in `StatsBuilderMixin.metrics`.

                The settings dict can contain the following keys:

                * `title`: Title of the metric. Defaults to the name.
                * `tags`: Single or multiple tags to associate this metric with.
                    If any of these tags is in `tags`, keeps this metric.
                * `check_{filter}` and `inv_check_{filter}`: Whether to check this metric against a
                    filter defined in `filters`. True (or False for inverse) means to keep this metric.
                * `calc_func` (required): Calculation function for custom metrics.
                    Should return either a scalar for one column/group, pd.Series for multiple columns/groups,
                    or a dict of such for multiple sub-metrics.
                * `resolve_calc_func`: whether to resolve `calc_func`. If the function can be accessed
                    by traversing attributes of this object, you can specify the path to this function
                    as a string (see `vectorbt.utils.attr_.deep_getattr` for the path format).
                    If `calc_func` is a function, arguments from merged metric settings are matched with
                    arguments in the signature (see below). If `resolve_calc_func` is False, `calc_func`
                    should accept (resolved) self and dictionary of merged metric settings.
                    Defaults to True.
                * `post_calc_func`: Function to post-process the result of `calc_func`.
                    Should accept (resolved) self, output of `calc_func`, and dictionary of merged metric settings,
                    and return whatever is acceptable to be returned by `calc_func`. Defaults to None.
                * `fill_wrap_kwargs`: Whether to fill `wrap_kwargs` with `to_timedelta` and `silence_warnings`.
                    Defaults to False.
                * `apply_to_timedelta`: Whether to apply `vectorbt.base.array_wrapper.ArrayWrapper.to_timedelta`
                    on the result. To disable this globally, pass `to_timedelta=False` in `settings`.
                    Defaults to False.
                * `pass_{arg}`: Whether to pass any argument from the settings (see below). Defaults to True if
                    this argument was found in the function's signature. Set to False to not pass.
                    If argument to be passed was not found, `pass_{arg}` is removed.
                * `resolve_path_{arg}`: Whether to resolve an argument that is meant to be an attribute of
                    this object and is the first part of the path of `calc_func`. Passes only optional arguments.
                    Defaults to True. See `vectorbt.utils.attr_.AttrResolver.resolve_attr`.
                * `resolve_{arg}`: Whether to resolve an argument that is meant to be an attribute of
                    this object and is present in the function's signature. Defaults to False.
                    See `vectorbt.utils.attr_.AttrResolver.resolve_attr`.
                * `template_mapping`: Mapping to replace templates in metric settings. Used across all settings.
                * Any other keyword argument that overrides the settings or is passed directly to `calc_func`.

                If `resolve_calc_func` is True, the calculation function may "request" any of the
                following arguments by accepting them or if `pass_{arg}` was found in the settings dict:

                * Each of `vectorbt.utils.attr_.AttrResolver.self_aliases`: original object
                    (ungrouped, with no column selected)
                * `group_by`: won't be passed if it was used in resolving the first attribute of `calc_func`
                    specified as a path, use `pass_group_by=True` to pass anyway
                * `column`
                * `metric_name`
                * `agg_func`
                * `silence_warnings`
                * `to_timedelta`: replaced by True if None and frequency is set
                * Any argument from `settings`
                * Any attribute of this object if it meant to be resolved
                    (see `vectorbt.utils.attr_.AttrResolver.resolve_attr`)

                Pass `metrics='all'` to calculate all supported metrics.
            tags (str or iterable): Tags to select.

                See `vectorbt.utils.tags.match_tags`.
            column (str): Name of the column/group.

                !!! hint
                    There are two ways to select a column: `obj['a'].stats()` and `obj.stats(column='a')`.
                    They both accomplish the same thing but in different ways: `obj['a'].stats()` computes
                    statistics of the column 'a' only, while `obj.stats(column='a')` computes statistics of
                    all columns first and only then selects the column 'a'. The first method is preferred
                    when you have a lot of data or caching is disabled. The second method is preferred when
                    most attributes have already been cached.
            group_by (any): Group or ungroup columns. See `vectorbt.base.column_grouper.ColumnGrouper`.
            agg_func (callable): Aggregation function to aggregate statistics across all columns.
                Defaults to mean.

                Should take `pd.Series` and return a const.

                Has only effect if `column` was specified or this object contains only one column of data.

                If `agg_func` has been overridden by a metric:

                * it only takes effect if global `agg_func` is not None
                * will raise a warning if it's None but the result of calculation has multiple values
            silence_warnings (bool): Whether to silence all warnings.
            template_mapping (mapping): Global mapping to replace templates.

                Gets merged over `template_mapping` from `StatsBuilderMixin.stats_defaults`.

                Applied on `settings` and then on each metric settings.
            filters (dict): Filters to apply.

                Each item consists of the filter name and settings dict.

                The settings dict can contain the following keys:

                * `filter_func`: Filter function that should accept resolved self and
                    merged settings for a metric, and return either True or False.
                * `warning_message`: Warning message to be shown when skipping a metric.
                    Can be a template that will be substituted using merged metric settings as mapping.
                    Defaults to None.
                * `inv_warning_message`: Same as `warning_message` but for inverse checks.

                Gets merged over `filters` from `StatsBuilderMixin.stats_defaults`.
            settings (dict): Global settings and resolution arguments.

                Extends/overrides `settings` from `StatsBuilderMixin.stats_defaults`.
                Gets extended/overridden by metric settings.
            metric_settings (dict): Keyword arguments for each metric.

                Extends/overrides all global and metric settings.

        For template logic, see `vectorbt.utils.template`.

        For defaults, see `StatsBuilderMixin.stats_defaults`.

        !!! hint
            There are two types of arguments: optional (or resolution) and mandatory arguments.
            Optional arguments are only passed if they are found in the function's signature.
            Mandatory arguments are passed regardless of this. Optional arguments can only be defined
            using `settings` (that is, globally), while mandatory arguments can be defined both using
            default metric settings and `{metric_name}_kwargs`. Overriding optional arguments using default
            metric settings or `{metric_name}_kwargs` won't turn them into mandatory. For this, pass `pass_{arg}=True`.

        !!! hint
            Make sure to resolve and then to re-use as many object attributes as possible to
            utilize built-in caching (even if global caching is disabled).

        ## Example

        See `vectorbt.portfolio.base` for examples.
        """
        # Resolve defaults
        if silence_warnings is None:
            silence_warnings = self.stats_defaults.get('silence_warnings',
                                                       False)
        template_mapping = merge_dicts(
            self.stats_defaults.get('template_mapping', {}), template_mapping)
        filters = merge_dicts(self.stats_defaults.get('filters', {}), filters)
        settings = merge_dicts(self.stats_defaults.get('settings', {}),
                               settings)
        metric_settings = merge_dicts(
            self.stats_defaults.get('metric_settings', {}), metric_settings)

        # Replace templates globally (not used at metric level)
        if len(template_mapping) > 0:
            sub_settings = deep_substitute(settings, mapping=template_mapping)
        else:
            sub_settings = settings

        # Resolve self
        reself = self.resolve_self(cond_kwargs=sub_settings,
                                   impacts_caching=False,
                                   silence_warnings=silence_warnings)

        # Prepare metrics
        if metrics is None:
            metrics = reself.stats_defaults.get('metrics', 'all')
        if metrics == 'all':
            metrics = reself.metrics
        if isinstance(metrics, dict):
            metrics = list(metrics.items())
        if isinstance(metrics, (str, tuple)):
            metrics = [metrics]

        # Prepare tags
        if tags is None:
            tags = reself.stats_defaults.get('tags', 'all')
        if isinstance(tags, str) and tags == 'all':
            tags = None
        if isinstance(tags, (str, tuple)):
            tags = [tags]

        # Bring to the same shape
        new_metrics = []
        for i, metric in enumerate(metrics):
            if isinstance(metric, str):
                metric = (metric, reself.metrics[metric])
            if not isinstance(metric, tuple):
                raise TypeError(
                    f"Metric at index {i} must be either a string or a tuple")
            new_metrics.append(metric)
        metrics = new_metrics

        # Handle duplicate names
        metric_counts = Counter(list(map(lambda x: x[0], metrics)))
        metric_i = {k: -1 for k in metric_counts.keys()}
        metrics_dct = {}
        for i, (metric_name, _metric_settings) in enumerate(metrics):
            if metric_counts[metric_name] > 1:
                metric_i[metric_name] += 1
                metric_name = metric_name + '_' + str(metric_i[metric_name])
            metrics_dct[metric_name] = _metric_settings

        # Check metric_settings
        missed_keys = set(metric_settings.keys()).difference(
            set(metrics_dct.keys()))
        if len(missed_keys) > 0:
            raise ValueError(
                f"Keys {missed_keys} in metric_settings could not be matched with any metric"
            )

        # Merge settings
        opt_arg_names_dct = {}
        custom_arg_names_dct = {}
        resolved_self_dct = {}
        mapping_dct = {}
        for metric_name, _metric_settings in list(metrics_dct.items()):
            opt_settings = merge_dicts(
                {name: reself
                 for name in reself.self_aliases},
                dict(column=column,
                     group_by=group_by,
                     metric_name=metric_name,
                     agg_func=agg_func,
                     silence_warnings=silence_warnings,
                     to_timedelta=None), settings)
            _metric_settings = _metric_settings.copy()
            passed_metric_settings = metric_settings.get(metric_name, {})
            merged_settings = merge_dicts(opt_settings, _metric_settings,
                                          passed_metric_settings)
            metric_template_mapping = merged_settings.pop(
                'template_mapping', {})
            template_mapping_merged = merge_dicts(template_mapping,
                                                  metric_template_mapping)
            template_mapping_merged = deep_substitute(template_mapping_merged,
                                                      mapping=merged_settings)
            mapping = merge_dicts(template_mapping_merged, merged_settings)
            merged_settings = deep_substitute(merged_settings, mapping=mapping)

            # Filter by tag
            if tags is not None:
                in_tags = merged_settings.get('tags', None)
                if in_tags is None or not match_tags(tags, in_tags):
                    metrics_dct.pop(metric_name, None)
                    continue

            custom_arg_names = set(_metric_settings.keys()).union(
                set(passed_metric_settings.keys()))
            opt_arg_names = set(opt_settings.keys())
            custom_reself = reself.resolve_self(
                cond_kwargs=merged_settings,
                custom_arg_names=custom_arg_names,
                impacts_caching=True,
                silence_warnings=merged_settings['silence_warnings'])

            metrics_dct[metric_name] = merged_settings
            custom_arg_names_dct[metric_name] = custom_arg_names
            opt_arg_names_dct[metric_name] = opt_arg_names
            resolved_self_dct[metric_name] = custom_reself
            mapping_dct[metric_name] = mapping

        # Filter metrics
        for metric_name, _metric_settings in list(metrics_dct.items()):
            custom_reself = resolved_self_dct[metric_name]
            mapping = mapping_dct[metric_name]
            _silence_warnings = _metric_settings.get('silence_warnings')

            metric_filters = set()
            for k in _metric_settings.keys():
                filter_name = None
                if k.startswith('check_'):
                    filter_name = k[len('check_'):]
                elif k.startswith('inv_check_'):
                    filter_name = k[len('inv_check_'):]
                if filter_name is not None:
                    if filter_name not in filters:
                        raise ValueError(
                            f"Metric '{metric_name}' requires filter '{filter_name}'"
                        )
                    metric_filters.add(filter_name)

            for filter_name in metric_filters:
                filter_settings = filters[filter_name]
                _filter_settings = deep_substitute(filter_settings,
                                                   mapping=mapping)
                filter_func = _filter_settings['filter_func']
                warning_message = _filter_settings.get('warning_message', None)
                inv_warning_message = _filter_settings.get(
                    'inv_warning_message', None)
                to_check = _metric_settings.get('check_' + filter_name, False)
                inv_to_check = _metric_settings.get('inv_check_' + filter_name,
                                                    False)

                if to_check or inv_to_check:
                    whether_true = filter_func(custom_reself, _metric_settings)
                    to_remove = (to_check
                                 and not whether_true) or (inv_to_check
                                                           and whether_true)
                    if to_remove:
                        if to_check and warning_message is not None and not _silence_warnings:
                            warnings.warn(warning_message)
                        if inv_to_check and inv_warning_message is not None and not _silence_warnings:
                            warnings.warn(inv_warning_message)

                        metrics_dct.pop(metric_name, None)
                        custom_arg_names_dct.pop(metric_name, None)
                        opt_arg_names_dct.pop(metric_name, None)
                        resolved_self_dct.pop(metric_name, None)
                        mapping_dct.pop(metric_name, None)
                        break

        # Any metrics left?
        if len(metrics_dct) == 0:
            if not silence_warnings:
                warnings.warn("No metrics to calculate", stacklevel=2)
            return None

        # Compute stats
        arg_cache_dct = {}
        stats_dct = {}
        used_agg_func = False
        for i, (metric_name,
                _metric_settings) in enumerate(metrics_dct.items()):
            try:
                final_kwargs = _metric_settings.copy()
                opt_arg_names = opt_arg_names_dct[metric_name]
                custom_arg_names = custom_arg_names_dct[metric_name]
                custom_reself = resolved_self_dct[metric_name]

                # Clean up keys
                for k, v in list(final_kwargs.items()):
                    if k.startswith('check_') or k.startswith(
                            'inv_check_') or k in ('tags', ):
                        final_kwargs.pop(k, None)

                # Get metric-specific values
                _column = final_kwargs.get('column')
                _group_by = final_kwargs.get('group_by')
                _agg_func = final_kwargs.get('agg_func')
                _silence_warnings = final_kwargs.get('silence_warnings')
                if final_kwargs['to_timedelta'] is None:
                    final_kwargs[
                        'to_timedelta'] = custom_reself.wrapper.freq is not None
                to_timedelta = final_kwargs.get('to_timedelta')
                title = final_kwargs.pop('title', metric_name)
                calc_func = final_kwargs.pop('calc_func')
                resolve_calc_func = final_kwargs.pop('resolve_calc_func', True)
                post_calc_func = final_kwargs.pop('post_calc_func', None)
                use_caching = final_kwargs.pop('use_caching', True)
                fill_wrap_kwargs = final_kwargs.pop('fill_wrap_kwargs', False)
                if fill_wrap_kwargs:
                    final_kwargs['wrap_kwargs'] = merge_dicts(
                        dict(to_timedelta=to_timedelta,
                             silence_warnings=_silence_warnings),
                        final_kwargs.get('wrap_kwargs', None))
                apply_to_timedelta = final_kwargs.pop('apply_to_timedelta',
                                                      False)

                # Resolve calc_func
                if resolve_calc_func:
                    if not callable(calc_func):
                        passed_kwargs_out = {}

                        def _getattr_func(
                            obj: tp.Any,
                            attr: str,
                            args: tp.ArgsLike = None,
                            kwargs: tp.KwargsLike = None,
                            call_attr: bool = True,
                            _final_kwargs: tp.Kwargs = final_kwargs,
                            _opt_arg_names: tp.Set[str] = opt_arg_names,
                            _custom_arg_names: tp.Set[str] = custom_arg_names,
                            _arg_cache_dct: tp.Kwargs = arg_cache_dct
                        ) -> tp.Any:
                            if attr in final_kwargs:
                                return final_kwargs[attr]
                            if args is None:
                                args = ()
                            if kwargs is None:
                                kwargs = {}
                            if obj is custom_reself and _final_kwargs.pop(
                                    'resolve_path_' + attr, True):
                                if call_attr:
                                    return custom_reself.resolve_attr(
                                        attr,
                                        args=args,
                                        cond_kwargs={
                                            k: v
                                            for k, v in _final_kwargs.items()
                                            if k in _opt_arg_names
                                        },
                                        kwargs=kwargs,
                                        custom_arg_names=_custom_arg_names,
                                        cache_dct=_arg_cache_dct,
                                        use_caching=use_caching,
                                        passed_kwargs_out=passed_kwargs_out)
                                return getattr(obj, attr)
                            out = getattr(obj, attr)
                            if callable(out) and call_attr:
                                return out(*args, **kwargs)
                            return out

                        calc_func = custom_reself.deep_getattr(
                            calc_func,
                            getattr_func=_getattr_func,
                            call_last_attr=False)

                        if 'group_by' in passed_kwargs_out:
                            if 'pass_group_by' not in final_kwargs:
                                final_kwargs.pop('group_by', None)

                    # Resolve arguments
                    if callable(calc_func):
                        func_arg_names = get_func_arg_names(calc_func)
                        for k in func_arg_names:
                            if k not in final_kwargs:
                                if final_kwargs.pop('resolve_' + k, False):
                                    try:
                                        arg_out = custom_reself.resolve_attr(
                                            k,
                                            cond_kwargs=final_kwargs,
                                            custom_arg_names=custom_arg_names,
                                            cache_dct=arg_cache_dct,
                                            use_caching=use_caching)
                                    except AttributeError:
                                        continue
                                    final_kwargs[k] = arg_out
                        for k in list(final_kwargs.keys()):
                            if k in opt_arg_names:
                                if 'pass_' + k in final_kwargs:
                                    if not final_kwargs.get(
                                            'pass_' + k):  # first priority
                                        final_kwargs.pop(k, None)
                                elif k not in func_arg_names:  # second priority
                                    final_kwargs.pop(k, None)
                        for k in list(final_kwargs.keys()):
                            if k.startswith('pass_') or k.startswith(
                                    'resolve_'):
                                final_kwargs.pop(k, None)  # cleanup

                        # Call calc_func
                        out = calc_func(**final_kwargs)
                    else:
                        # calc_func is already a result
                        out = calc_func
                else:
                    # Do not resolve calc_func
                    out = calc_func(custom_reself, _metric_settings)

                # Call post_calc_func
                if post_calc_func is not None:
                    out = post_calc_func(custom_reself, out, _metric_settings)

                # Post-process and store the metric
                multiple = True
                if not isinstance(out, dict):
                    multiple = False
                    out = {None: out}
                for k, v in out.items():
                    # Resolve title
                    if multiple:
                        if title is None:
                            t = str(k)
                        else:
                            t = title + ': ' + str(k)
                    else:
                        t = title

                    # Check result type
                    if checks.is_any_array(v) and not checks.is_series(v):
                        raise TypeError(
                            "calc_func must return either a scalar for one column/group, "
                            "pd.Series for multiple columns/groups, or a dict of such. "
                            f"Not {type(v)}.")

                    # Handle apply_to_timedelta
                    if apply_to_timedelta and to_timedelta:
                        v = custom_reself.wrapper.to_timedelta(
                            v, silence_warnings=_silence_warnings)

                    # Select column or aggregate
                    if checks.is_series(v):
                        if _column is not None:
                            v = custom_reself.select_one_from_obj(
                                v,
                                custom_reself.wrapper.regroup(_group_by),
                                column=_column)
                        elif _agg_func is not None and agg_func is not None:
                            v = _agg_func(v)
                            used_agg_func = True
                        elif _agg_func is None and agg_func is not None:
                            if not _silence_warnings:
                                warnings.warn(
                                    f"Metric '{metric_name}' returned multiple values "
                                    f"despite having no aggregation function",
                                    stacklevel=2)
                            continue

                    # Store metric
                    if t in stats_dct:
                        if not _silence_warnings:
                            warnings.warn(f"Duplicate metric title '{t}'",
                                          stacklevel=2)
                    stats_dct[t] = v
            except Exception as e:
                warnings.warn(f"Metric '{metric_name}' raised an exception",
                              stacklevel=2)
                raise e

        # Return the stats
        if reself.wrapper.get_ndim(group_by=group_by) == 1:
            return pd.Series(stats_dct,
                             name=reself.wrapper.get_name(group_by=group_by))
        if column is not None:
            return pd.Series(stats_dct, name=column)
        if agg_func is not None:
            if used_agg_func and not silence_warnings:
                warnings.warn(
                    f"Object has multiple columns. Aggregating using {agg_func}. "
                    f"Pass column to select a single column/group.",
                    stacklevel=2)
            return pd.Series(stats_dct, name='agg_func_' + agg_func.__name__)
        new_index = reself.wrapper.grouper.get_columns(group_by=group_by)
        stats_df = pd.DataFrame(stats_dct, index=new_index)
        return stats_df
Example #7
0
    def resolve_attr(self,
                     attr: str,
                     args: tp.ArgsLike = None,
                     cond_kwargs: tp.KwargsLike = None,
                     kwargs: tp.KwargsLike = None,
                     custom_arg_names: tp.Optional[tp.Container[str]] = None,
                     cache_dct: tp.KwargsLike = None,
                     use_caching: bool = True,
                     passed_kwargs_out: tp.KwargsLike = None) -> tp.Any:
        """Resolve an attribute using keyword arguments and built-in caching.

        * If `attr` is a property, returns its value.
        * If `attr` is a method, passes `*args`, `**kwargs`, and `**cond_kwargs` with keys found in the signature.
        * If `attr` is a property and there is a `get_{arg}` method, calls the `get_{arg}` method.

        Won't cache if `use_caching` is False or any passed argument is in `custom_arg_names`.

        Use `passed_kwargs_out` to get keyword arguments that were passed."""
        # Resolve defaults
        if custom_arg_names is None:
            custom_arg_names = list()
        if cache_dct is None:
            cache_dct = {}
        if args is None:
            args = ()
        if kwargs is None:
            kwargs = {}
        if passed_kwargs_out is None:
            passed_kwargs_out = {}
        final_kwargs = merge_dicts(cond_kwargs, kwargs)

        # Resolve attribute
        cls = type(self)
        _attr = self.pre_resolve_attr(attr, final_kwargs=final_kwargs)
        if 'get_' + attr in dir(cls):
            _attr = 'get_' + attr
        if inspect.ismethod(getattr(cls, _attr)) or inspect.isfunction(getattr(cls, _attr)):
            attr_func = getattr(self, _attr)
            attr_func_kwargs = dict()
            attr_func_arg_names = get_func_arg_names(attr_func)
            custom_k = False
            for k, v in final_kwargs.items():
                if k in attr_func_arg_names or k in kwargs:
                    if k in custom_arg_names:
                        custom_k = True
                    attr_func_kwargs[k] = v
                    passed_kwargs_out[k] = v
            if use_caching and not custom_k and attr in cache_dct:
                out = cache_dct[attr]
            else:
                out = attr_func(*args, **attr_func_kwargs)
                if use_caching and not custom_k:
                    cache_dct[attr] = out
        else:
            if use_caching and attr in cache_dct:
                out = cache_dct[attr]
            else:
                out = getattr(self, _attr)
                if use_caching:
                    cache_dct[attr] = out
        out = self.post_resolve_attr(attr, out, final_kwargs=final_kwargs)
        return out