# 自定义指标 (/zh/features/indicators/indicator-development)

如何将自己的公式变成可扫描参数并重复使用的指标？VBT 指标工厂将任意 Python 或 Numba 函数封装成具有参数网格、带标签输出、信号辅助方法和绘图能力的指标类，让 Python
自定义指标与内置指标具有相同使用方式。

```python title="Build a volatility-adjusted momentum indicator and test six parameter pairs"
>>> @njit
... def vam_nb(close, fast, slow):  # (1)
...     momentum = close / vbt.nb.fshift_nb(close, fast) - 1
...     returns = close / vbt.nb.fshift_nb(close, 1) - 1
...     volatility = vbt.nb.rolling_std_nb(returns, slow)
...     return momentum / (volatility * np.sqrt(fast))

>>> VAM = vbt.IF(
...     class_name="VAM",
...     short_name="vam",
...     input_names=["close"],
...     param_names=["fast", "slow"],
...     output_names=["vam"],
... ).with_apply_func(vam_nb, takes_1d=False, fast=10, slow=50)  # (2)

>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], start="2023-01-01", end="2025-01-01")
>>> vam = VAM.run(
...     data.close,
...     fast=vbt.Param([5, 10, 20, 40], condition="fast < slow"),  # (3)
...     slow=vbt.Param([10, 20, 40]),
...     param_product=True,
... )
>>> vam.vam.shape
(731, 12)
>>> vam.vam.columns.droplevel("symbol").unique().tolist()
[(5, 10), (5, 20), (5, 40), (10, 20), (10, 40), (20, 40)]

>>> entries = vam.vam_crossed_above(1)  # (4)
>>> exits = vam.vam_crossed_below(0)
>>> pf = vbt.PF.from_signals(data, entries, exits)
>>> pf.total_return.unstack("symbol").round(3)
symbol             BTC-USD  ETH-USD
vam_fast vam_slow
5        10          0.717   -0.187
         20          0.388    0.454
         40          0.771    0.228
10       20          0.576    0.131
         40          1.119    0.000
20       40          1.322   -0.004
```

1.  `fast` 根 K 线上的动量除以 `slow` 根 K 线上的波动率，使用编译辅助函数编写，可处理整个数组。
2.  对每种参数组合，函数接收每资产一列的二维数组。工厂还设置默认参数值。
3.  在 12 种组合中，仅保留快速窗口短于慢速窗口的组合。
4.  每个输出都获得 `vam_crossed_above` 等比较方法，用于构建信号。

函数只有十行。工厂添加参数网格、列标签、信号方法及指标需要的其他能力，结果可直接进入回测。

## 从函数到指标 \[#from-a-function-to-an-indicator]

`vbt.IF` 接受输入、参数和输出的名称，以及计算它们的函数。函数可以逐列或处理整个数组，返回一个或多个输出，并将额外结果保存在原地输出中。生成类拥有带默认值的 `run`
方法、可读输出属性、各输出的 `crossed_above`、`above` 等比较辅助方法，以及绘图能力。输出像 pandas 对象一样支持算术，因此可直接使用 `vam.vam * 2` 或
`fast.ma - slow.ma`。

函数默认接收 NumPy 数组，以保持可编译。传入 `keep_pd=True` 则改为接收 pandas 对象，例如调用 pandas 滚动或分组方法。这时函数必须是普通 Python，因为
Numba 无法编译接收 pandas
对象的代码。常规指标输出在每种参数组合下与输入形状一致。返回每次运行一个数字或其他长度数组等结果的函数，更适合参数化装饰器，详见[参数化任意函数](/zh/features/optimization/strategy-optimization/#parameterize-any-function)。

## 复用 pandas 计算 \[#reuse-your-pandas-calculations]

您可以从 notebook 中已可运行的计算开始。这里普通 Python 函数返回滚动均值，以及价格相对均值的百分比距离。`keep_pd=True` 让它直接使用
pandas，工厂则处理窗口、标的和输出标签。

```python title="Turn a pandas calculation into a reusable indicator"
>>> def distance_from_mean(close, window):
...     mean = close.rolling(window).mean()
...     return mean, 100 * (close / mean - 1)

>>> Distance = vbt.IF(
...     class_name="Distance",
...     short_name="dist",
...     input_names=["close"],
...     param_names=["window"],
...     output_names=["mean", "distance"],
... ).with_apply_func(distance_from_mean, keep_pd=True)
>>> close = pd.DataFrame({
...     "A": [10, 11, 12, 11, 13, 15],
...     "B": [20, 19, 18, 19, 17, 15],
... }, index=pd.date_range("2025-01-01", periods=6))
>>> close.columns.name = "symbol"
>>> result = Distance.run(close, window=[2, 3])
>>> result.output_names
('mean', 'distance')
>>> result.distance.iloc[-1].unstack("symbol").round(2)
symbol           A      B
dist_window
2             7.14  -6.25
3            15.38 -11.76
```

在这些虚构价格上，A 最终高于滚动均值，B
则低于均值。均值和距离都可继续用于绘图、比较或其他计算。您可以将距离作为模型特征或均值回归策略阈值，再通过[信号回测](/zh/features/backtesting/signal-backtesting/)测试规则。

## 参数与网格 \[#parameters-and-grids]

参数接受单个值、列表或 `vbt.Param` 对象。列表默认逐元素配对，`param_product=True`
构建全部组合。条件可以按名称筛选组合，如上所示；每个参数成为以指标短名称命名的列层级，因此可按参数选择和分组结果。

## 输入 \[#inputs]

不同形状的输入会相互广播。单个价格序列与参数组合，每种组合产生一列；多个资产则扩展网格，正如上面的两个资产与六组参数。设置 `takes_1d=True`
后，函数每次接收一列，由工厂负责遍历各列。如果每个资产需要自己的设置，而非相同网格，`per_column=True` 会将每个输入列与对应参数组合配对。

## 将公式作为指标 \[#formulas-as-indicators]

指标也可写成表达式，例如
`(close - low) / (high - low)`，按名称解析输入、参数和辅助函数，如下方[指标表达式](#indicator-expressions)功能介绍所示。表达式适合快速尝试想法，工厂将其转换为与编译函数相同类型的类。

## 性能 \[#performance]

编译函数以 Numba
速度运行整个网格。如果多个组合需要同一中间结果，例如多组参数共享一条均线，工厂可在缓存函数中计算一次后复用。逐列工作的指标可以使用并行线程，为一维编写的指标也能自动跨列并行，如[原生一维指标](#1d-native-indicators)和[可并行指标](#parallelizable-indicators)功能介绍所示。滚动指标需要预热期，因此足够
K 线出现前，初始值为 `NaN`。

## 组合与复用 \[#composition-and-reuse]

指标可以组合：一个输出成为另一个输入，多个指标堆叠为一个对象，结果像 DataFrame 一样索引。注册后，自定义指标可通过 `vbt.indicator()` 按名称查找，并通过数据对象的
`data.run()`
运行，与内置指标相同。下方[指标搜索](#indicator-search)功能介绍展示跨全部集成库查找指标，[机器学习指标](#indicators-for-ml)则将多个指标转换为特征矩阵。

注册也能将指标组织为具名分组，例如趋势过滤器或波动率特征。通过数据对象运行整个分组，可在新数据集上重建相同特征集，适合比较应接收相同输入的模型。

通过 `lazy_outputs` 可以添加派生属性，仅在请求时计算。例如，返回多条线的指标可将线间差值作为另一属性公开。这让相关计算保存在一起，同时可自行选择检查哪些结果。随新 K
线更新的计算，请参阅[流式指标](/zh/features/indicators/streaming-indicators/)。

## 指标搜索 \[#indicator-search]

新增于 1.11.1

✅ VBT 实现或集成了 500 多个指标，难以逐一记住。为了更容易查找，现提供多种全局搜索指标的新方法。

```python title="List all moving average indicators"
>>> vbt.IF.list_indicators("*ma")
[
    'vbt:MA',
    'talib:DEMA',
    'talib:EMA',
    'talib:KAMA',
    'talib:MA',
    ...
    'technical:ZEMA',
    'technical:ZLEMA',
    'technical:ZLHMA',
    'technical:ZLMA'
]

>>> vbt.indicator("technical:ZLMA")  # (1)
vectorbtpro.indicators.factory.technical.ZLMA
```

1.  与 `vbt.IF.get_indicator` 相同。

## 机器学习指标 \[#indicators-for-ml]

新增于 1.8.2

✅ 想将指标作为机器学习模型特征？无需逐个运行：可以让 VBT 在给定数据实例上运行某个指标包的全部指标。数据实例识别各指标输入名称，提供所需数据，也可轻松修改每个指标的默认参数。

```python title="Run all talib indicators on entire BTC-USD history"
>>> data = vbt.YFData.pull("BTC-USD")
>>> features = data.run("talib", mavp=vbt.run_arg_dict(periods=14))
>>> features.shape
(3046, 175)
```

## 原生一维指标 \[#1d-native-indicators]

新增于 1.0.10

✅ 此前，自定义指标只能接受二维输入数组，用户必须相应调整所有函数。现在，指标工厂可以按列拆分每个输入数组，每次传入一列，使设计原本面向一维数据的指标（例如 TA-Lib！）轻松得多。

```python title="Create a TA-Lib powered STOCHRSI indicator"
>>> import talib

>>> params = dict(
...     rsi_period=14,
...     fastk_period=5,
...     slowk_period=3,
...     slowk_matype=0,
...     slowd_period=3,
...     slowd_matype=0
... )

>>> def stochrsi_1d(close, *args):
...     rsi = talib.RSI(close, args[0])
...     k, d = talib.STOCH(rsi, rsi, rsi, *args[1:])
...     return rsi, k, d

>>> STOCHRSI = vbt.IF(
...     input_names=["close"],
...     param_names=list(params.keys()),
...     output_names=["rsi", "k", "d"]
... ).with_apply_func(stochrsi_1d, takes_1d=True, **params)

>>> data = vbt.YFData.pull("BTC-USD", start="2022-01", end="2022-06")
>>> stochrsi = STOCHRSI.run(data.close)
>>> fig = stochrsi.k.rename("%K").vbt.plot()
>>> stochrsi.d.rename("%D").vbt.plot(fig=fig)
>>> fig.show()
```

基于 TA-Lib 的 BTC-USD 随机 RSI：%K 与 %D. [Figure data (JSON)](/assets/figures/features/indicators/stochastic-rsi.73211844ad8e.json)

## 可并行指标 \[#parallelizable-indicators]

新增于 1.0.10

✅ 指标工厂处理参数组合时，可分配到多个线程、进程，甚至云端。这对运行较慢的指标帮助很大 🐌

```python title="Benchmark a serial and multithreaded rolling min-max indicator"
>>> @njit
... def minmax_nb(close, window):
...     return (
...         vbt.nb.rolling_min_nb(close, window),
...         vbt.nb.rolling_max_nb(close, window)
...     )

>>> MINMAX = vbt.IF(
...     class_name="MINMAX",
...     input_names=["close"],
...     param_names=["window"],
...     output_names=["min", "max"]
... ).with_apply_func(minmax_nb, window=14)

>>> data = vbt.YFData.pull("BTC-USD")
```

```python
>>> %%timeit
>>> minmax = MINMAX.run(
...     data.close,
...     np.arange(2, 200),
...     jitted_loop=True
... )
420 ms ± 2.05 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

```python
>>> %%timeit
>>> minmax = MINMAX.run(
...     data.close,
...     np.arange(2, 200),
...     jitted_loop=True,
...     jitted_warmup=True,  # (1)
...     execute_kwargs=dict(engine="threadpool", n_chunks="auto")  # (2)
... )
120 ms ± 355 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

1.  先运行一种参数组合来编译指标，再以多线程运行其他组合。
2.  每个线程一个 Numba 循环，线程数与核心数相同。

## 指标表达式 \[#indicator-expressions]

新增于 1.0.8

✅ 现在可以从表达式创建指标。指标表达式是表示 Python
代码的普通字符串，并增强了多种扩展。借助独特格式和内置匹配机制，指标工厂自动推导全部所需信息，包括输入、参数、输出、NumPy、VBT、TA-Lib 函数，甚至复杂指标。设计指标从未如此轻松！

```python title="Build a MACD indicator from an expression"
>>> data = vbt.YFData.pull("BTC-USD", start="2020", end="2021")

>>> expr = """
... MACD:
... fast_ema = @talib_ema(close, @p_fast_w)
... slow_ema = @talib_ema(close, @p_slow_w)
... macd = fast_ema - slow_ema
... signal = @talib_ema(macd, @p_signal_w)
... macd, signal
... """
>>> MACD = vbt.IF.from_expr(expr, fast_w=12, slow_w=26, signal_w=9)  # (1)
>>> macd = MACD.run(data.close)
>>> fig = macd.macd.rename("MACD").vbt.plot()
>>> macd.signal.rename("Signal").vbt.plot(fig=fig)
>>> fig.show()
```

1.  无需手动设置 `input_names`、`param_names` 或其他信息。

使用表达式构建的 2020 年 BTC-USD MACD 线与信号线. [Figure data (JSON)](/assets/figures/features/indicators/indicator-expressions.6bbc8e453580.json)
