# 回测引擎 (/zh/features/backtesting/backtesting-engine)

能否在 Python 中回测自定义交易逻辑，同时保留速度和控制力？VBT 是一个 Python
回测引擎，用编译代码逐根模拟订单，将每个资产与参数组合放入同一次向量化模拟的不同列，并返回可检查每笔订单、交易和持仓的投资组合。可从入场与离场信号开始，回放已有订单列表，或构建在模拟期间响应现金与持仓的自定义逻辑。每种方式都提供相同的投资组合分析工具。

```python title="Backtest four moving average pairs on two assets in one call"
>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], start="2021-01-01", end="2024-01-01")
>>> fast = vbt.MA.run(data.close, window=[10, 10, 20, 20], short_name="fast")
>>> slow = vbt.MA.run(data.close, window=[50, 100, 50, 100], short_name="slow")
>>> entries = fast.ma_crossed_above(slow)
>>> exits = fast.ma_crossed_below(slow)

>>> pf = vbt.PF.from_signals(data, entries, exits, fees=0.001)  # (1)
>>> pf.total_return.unstack("symbol")
symbol                    BTC-USD   ETH-USD
fast_window slow_window
10          50           0.346309  0.720064
            100          0.577382 -0.151935
20          50          -0.139554  0.017887
            100          0.365233 -0.110708

>>> pf[(10, 50, "ETH-USD")].plot(settings=dict(bm_returns=False)).show()  # (2)
```

1.  八个回测作为同一模拟的八个带标签列运行，各自拥有现金。
2.  按标签选择任意列，像单个回测一样分析。

每个信号在出现当根收盘成交，每笔订单收取 0.1%
手续费。若要在下一根执行，参见[符合因果关系的订单时机](/zh/features/backtesting/backtest-realism/#causal-order-timing)。

2021 至 2023 年 ETH-USD 10 日与 50 日均线交叉订单、交易收益和累计收益. [Figure data (JSON)](/assets/figures/features/backtesting/backtesting-engine-grid-column.e01add8da42c.json)

## 三种模拟方式 \[#three-ways-to-simulate]

每种模拟方法返回相同的 `Portfolio` 对象，差别在于如何描述交易决策：

| 方法                       | 您提供             | 用于            |
| ------------------------ | --------------- | ------------- |
| `vbt.PF.from_orders`     | 逐根数量、价格和方向      | 再平衡、目标权重、订单列表 |
| `vbt.PF.from_signals`    | 入场与离场信号，以及止损和限价 | 大多数规则策略       |
| `vbt.PF.from_order_func` | 返回订单的编译函数       | 依赖投资组合状态的逻辑   |

`from_orders` 最直接：提供数量与价格，引擎处理现金和执行。若目标权重需结合止损与限价，`from_signals` 可通过 `order_mode=True`
接受，详见[将目标作为信号](/zh/features/backtesting/signal-backtesting/#targets-as-signals)。两种方法每列每根最多成交一个订单。若要多个，例如开盘买入、收盘卖出，可使用灵活订单函数，详见[订单函数](/zh/features/backtesting/event-driven-backtesting/#order-functions)。

`from_orders` 和 `from_signals` 接受模拟前准备的数组，执行仍响应当前账户。例如，目标权重根据投资组合净值和已有持仓转化为订单数量。`from_signals` 和
`from_order_func`
还接受在模拟期间决策的回调，详见[事件驱动回测](/zh/features/backtesting/event-driven-backtesting/)。无论静态或动态，方法共享订单执行规则。这里，同一纯多头策略的两种表达得到相同订单：

```python title="Run one strategy through two simulation methods"
>>> close = data.close["BTC-USD"]
>>> entries = close.vbt.crossed_above(close.rolling(50).mean())
>>> exits = close.vbt.crossed_below(close.rolling(50).mean())
>>> pf_signals = vbt.PF.from_signals(close, entries, exits)

>>> size = pd.Series(np.nan, index=close.index)  # (1)
>>> size[entries] = np.inf
>>> size[exits] = -np.inf
>>> pf_orders = vbt.PF.from_orders(close, size, direction="longonly")

>>> cols = ["Index", "Size", "Price", "Side"]
>>> a = pf_signals.orders.readable.rename(columns={"Fill Index": "Index"})[cols]
>>> b = pf_orders.orders.readable[cols]
>>> a.equals(b), len(a)
(True, 51)
```

1.  `NaN` 不下单，正无穷用全部现金买入，负无穷卖出全部持仓。

高层方法之下是接受普通 NumPy 数组的编译函数，可从自己的 Numba
代码调用，详见[信号回测](/zh/features/backtesting/signal-backtesting/#calling-the-kernel-directly)。

## 一次调用运行多个回测 \[#many-backtests-in-one-call]

输入相互广播。八列信号、两列价格和标量手续费产生八个回测，参数值成为可选择、分组和绘图的列层级。默认每列为独立账户。

分组可改变这一点。`group_by` 将多列合并为报告中的一个组合，`cash_sharing=True`
使它们使用同一现金余额，用于模拟多资产组合。按参数层级对首个网格分组，会将八个单资产回测转为四个双资产组合：

```python title="Turn the grid into four two-asset portfolios with shared cash"
>>> pf = vbt.PF.from_signals(
...     data,
...     fast.ma_crossed_above(slow),
...     fast.ma_crossed_below(slow),
...     size=0.5,  # (1)
...     size_type="valuepercent",
...     fees=0.001,
...     group_by=["fast_window", "slow_window"],  # (2)
...     cash_sharing=True,
... )
>>> pf.total_return
fast_window  slow_window
10           50             0.570373
             100            0.186409
20           50            -0.055170
             100            0.133235
Name: total_return, dtype: float64
```

1.  每次入场投入组当前净值的一半，受剩余现金限制。
2.  参数相同的列组成一组，因此 BTC 与 ETH 共享账户。

模拟器在组内逐根延续现金与持仓。速度来自编译循环，以及多个独立组并排运行。可用[并行执行与缓存](/zh/features/performance/parallel-execution-and-caching/)页的选项扩展参数扫描。

!!! tip "每列一个持仓"
    一列持有一个净头寸。可通过[加减仓](/zh/features/backtesting/signal-backtesting/#accumulation-and-entry-ladders)增加持仓；也可将同一品种的各笔独立管理交易放入各自列，再分组，并可共享现金。[信号展开](/zh/features/indicators/trading-signals/#signal-unraveling)可从一个信号数组构建这些列。

## 模拟窗口 \[#simulation-windows]

指标在首个信号之前需要历史数据。无需切片数据，传入 `sim_start` 和 `sim_end` 即可：指标看到完整历史，模拟则在指定根从空仓开始，并在结束根之前停止。

```python title="Simulate only 2022 and 2023 while the indicator uses 2021"
>>> pf = vbt.PF.from_signals(close, entries, exits, sim_start="2022-01-01")
>>> pf.stats(["start_index", "end_index", "total_return", "total_trades"])
Start Index         2022-01-01 00:00:00+00:00
End Index           2023-12-31 00:00:00+00:00
Total Return [%]                    25.991572
Total Trades                               18
Name: BTC-USD, dtype: object
```

窗口可逐列不同，`sim_start="auto"`
从各列首个信号开始，回调也可提前结束一组。度量、记录与图表都遵循窗口，详见下方[模拟区间](#simulation-ranges)。也可在已有投资组合上分析不同日期区间，无须重跑回测。

## 以记录作为输入 \[#inputs-as-records]

可直接从时间戳与资产列匹配的 pandas Series 或 DataFrame 获取价格和信号。可使用已有数据流程或 VBT
的[市场数据集成](/zh/features/data/market-data-sources/)。

某些策略是一份事件列表：在某日期买入某品种。利用数据封装器容易构建稠密数组，例如先 `data.symbol_wrapper.fill(False)`，再用 `.loc`
赋值。也可完全省略数组，直接以记录传入事件，见下方[索引记录](#index-records)。

记录还可将券商、电子表格或其他回测器的成交日志转为投资组合。日志各列对应模拟方法的一个参数：

```python title="Replay a year of manual stock trades from a fill log"
>>> data = vbt.YFData.pull(["AAPL", "NVDA"], start="2024-01-01", end="2025-01-01")
>>> fills = pd.DataFrame(
...     [
...         ("2024-02-05", "AAPL", 50, 188.20, 1.0),
...         ("2024-03-11", "NVDA", 100, 86.40, 1.0),
...         ("2024-04-22", "AAPL", -50, 166.90, 1.0),
...         ("2024-06-18", "NVDA", -60, 131.70, 1.0),
...         ("2024-08-05", "NVDA", 60, 101.20, 1.0),
...         ("2024-11-25", "NVDA", -100, 137.40, 1.0),
...     ],
...     columns=["date", "symbol", "size", "price", "fixed_fees"],  # (1)
... )
>>> pf = vbt.PF.from_orders(
...     data,
...     records=fills,
...     init_cash=20_000,
...     group_by=True,
...     cash_sharing=True,
... )
>>> pf.trades.readable[["Column", "Size", "Avg Entry Price", "Avg Exit Price", "PnL", "Return"]]
  Column   Size  Avg Entry Price  Avg Exit Price     PnL    Return
0   AAPL   50.0           188.20           166.9 -1067.0 -0.113390
1   NVDA   60.0            86.40           131.7  2716.4  0.523997
2   NVDA  100.0            95.28           137.4  4209.6  0.441814

>>> pf.total_return
np.float64(0.29294999999999993)
```

1.  `date` 和 `symbol` 定位订单，其他列成为该根的 `size`、`price` 与 `fixed_fees`。

NVDA 部分卖出将 60 股作为一笔交易平仓，余下 40 股与八月买入合并，平均价为 95.28。日志成为组合后，可检查度量与图表，或用不同手续费和离场日期重放订单，探索假设情景。

落在 K 线内部的时间戳会映射到数据中的下一根，因此应先将日内成交时间向下取整至 K 线起点，例如日线用
`fills["date"].dt.floor("1D")`。同一品种同根的后续记录覆盖前一条，因此传入前应合并同根成交。

## 运行前检查 \[#inspect-before-running]

模拟前，预处理器转换参数：字符串变枚举、输入广播至最终形状、模板填充、默认值应用。传入 `return_prep_result=True`
可取得该结果而非投资组合，检查或修改参数后再运行。下方[投资组合预处理器](#portfolio-preparers)逐步展示。

## 编译一次，快速重复运行 \[#compile-once-rerun-fast]

研究意味着反复运行同一模拟。以下设置有助于保持速度：

*   **只编译一次。** 带回调的模拟器在每个新 Python 会话中编译。`staticized=True` 让 VBT 为回调写出可缓存模拟器副本，后续会话跳过编译。回调源码、模拟设置或 VBT
    版本改变时，副本自动重新生成。不会检测回调导入的其他模块改动，修改它们后应传入
    `staticized=dict(override=True)`。无回调模拟使用已缓存模拟器，因此每次安装只编译一次。
*   **保存将要读取的内容。** 默认模拟存储订单，在请求时重建余额和收益。`save_returns=True` 及相关标志改为模拟期间计算，以内存换速度。
*   **注意内存。** 每个完整 `float64` 数组占行数 × 列数 × 8 字节。20 年加密货币日线、10,000 列，每个数组约 580
    MB，模拟会保留多个。大网格应分块，参见[并行执行与缓存](/zh/features/performance/parallel-execution-and-caching/)。

## 以属性读取结果 \[#results-as-attributes]

每项结果都是投资组合属性。`pf.sharpe_ratio` 按默认设置返回并缓存，`pf.get_sharpe_ratio(risk_free=0.02)`
则接受参数。属性也可由您提供的数组计算，因此无需重跑模拟，就能测试不同基准或覆盖某个序列。

添加自定义度量或行为时，可继承 `vbt.Portfolio`。`from_signals` 等类方法返回子类实例。若需重新标记结果，可用
`pf.replace(wrapper=dict(columns=...), nested_=True)` 创建新组合。

!!! info "文档"
    会员专享的[投资组合文档（英文）](https://members.vectorbt.pro/documentation/portfolio/)介绍从订单执行到记录与封装的模拟生命周期。

## 投资组合预处理器 \[#portfolio-preparers]

新增于 1.12.0

✅ 参数传给 `Portfolio.from_signals` 等模拟方法时，会经过复杂预处理流程，转换为适合 Numba 的格式。通常包括枚举映射、广播、数据类型检查、模板替换等步骤。为使 VBT
更透明，该流程已移到独立类，让您完整控制到达 Numba 函数的参数！甚至可扩展预处理器，为自己的模拟器自动准备参数。

```python title="Get a prepared argument, modify it, and use in a new simulation"
>>> data = vbt.YFData.pull("BTC-USD", end="2017-01")
>>> prep_result = vbt.PF.from_holding(
...     data,
...     stop_ladder="uniform",
...     tp_stop=vbt.Param([
...         [0.1, 0.2, 0.3, 0.4, 0.5],
...         [0.4, 0.5, 0.6],
...     ], keys=["tp_ladder_1", "tp_ladder_2"]),
...     return_prep_result=True  # (1)
... )
>>> prep_result.target_args["tp_stop"]  # (2)
array([[0.1, 0.4],
       [0.2, 0.5],
       [0.3, 0.6],
       [0.4, nan],
       [0.5, nan]])

>>> new_tp_stop = prep_result.target_args["tp_stop"] + 0.1
>>> new_prep_result = prep_result.replace(target_args=dict(tp_stop=new_tp_stop), nested_=True)  # (3)
>>> new_prep_result.target_args["tp_stop"]
array([[0.2, 0.5],
       [0.3, 0.6],
       [0.4, 0.7],
       [0.5, nan],
       [0.6, nan]])

>>> pf = vbt.PF.from_signals(new_prep_result)  # (4)
>>> pf.total_return
tp_stop
tp_ladder_1    0.4
tp_ladder_2    0.6
Name: total_return, dtype: float64

>>> sim_out = new_prep_result.target_func(**new_prep_result.target_args)  # (5)
>>> pf = vbt.PF(sim_out=sim_out, **new_prep_result.pf_args)
>>> pf.total_return
tp_stop
tp_ladder_1    0.4
tp_ladder_2    0.6
Name: total_return, dtype: float64
```

1.  返回预处理结果。
2.  包含两个属性：目标参数 `target_args` 和投资组合参数 `pf_args`。
3.  替换参数。它位于另一个字典 `target_args` 内，因此需启用 `nested_`。结果为新的 `PFPrepResult` 实例。
4.  将新预处理结果作为基础模拟方法的第一个参数。
5.  或手动模拟！

## 静态化 \[#staticization]

新增于 1.11.0

✅ Numba
的主要限制之一是：作为参数传入的函数（即回调）会使主函数无法缓存，每次新运行时都必须反复编译。这尤其影响模拟器，因为编译可能长达一分钟。现在可以使用“静态化”技巧：先用特殊语法标记函数源码，再提取（也称“裁切”✂️）代码，将回调从参数中移除，修改为可缓存版本并保存到
Python 文件。下次调用执行可缓存版本。听起来复杂？看下方示例！

```python title="Define the signal function in a Python file, here signal_func_nb.py"
from vectorbtpro import *

@njit
def signal_func_nb(ctx, fast_sma, slow_sma):  # (1)
    long = vbt.pf_nb.iter_crossed_above_nb(ctx, fast_sma, slow_sma)
    short = vbt.pf_nb.iter_crossed_below_nb(ctx, fast_sma, slow_sma)
    return long, False, short, False
```

1.  确保函数名称与回调参数完全相同。

```python title="Run a staticized simulation. Running the script again won't re-compile."
>>> data = vbt.YFData.pull("BTC-USD")
>>> pf = vbt.PF.from_signals(
...     data,
...     signal_func_nb="signal_func_nb.py",  # (1)
...     signal_args=(vbt.Rep("fast_sma"), vbt.Rep("slow_sma")),
...     broadcast_named_args=dict(
...         fast_sma=data.run("sma", 20, hide_params=True, unpack=True),
...         slow_sma=data.run("sma", 50, hide_params=True, unpack=True)
...     ),
...     staticized=True  # (2)
... )
```

1.  函数所在模块的路径。
2.  完成所有自动处理 ✨

## 预计算 \[#pre-computation]

新增于 1.0.10

✅ 内存和执行速度之间存在取舍：1000 列数据通常比单列数据处理 1000 次快得多，但也需要 1000 倍内存。因此，模拟阶段 VBT
主要生成订单，余额、净值与收益等其他属性在分析阶段按需重建。性能最重要时，现在可通过参数在模拟期间预先计算这些属性！⏩

```python title="Benchmark a random portfolio with 1000 columns without and with pre-computation"
>>> data = vbt.YFData.pull("BTC-USD")
```

```python
>>> %%timeit  # (1)
>>> for n in range(1000):
...     pf = vbt.PF.from_random_signals(data, n=n, seed=42, save_returns=False)
...     pf.sharpe_ratio
15 s ± 829 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

1.  节省 RAM，但性能较差。

```python
>>> %%timeit
>>> pf = vbt.PF.from_random_signals(data, n=np.arange(1000).tolist(), seed=42, save_returns=False)
>>> pf.sharpe_ratio
855 ms ± 6.26 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

```python
>>> %%timeit
>>> pf = vbt.PF.from_random_signals(data, n=np.arange(1000).tolist(), seed=42, save_returns=True)
>>> pf.sharpe_ratio
593 ms ± 7.07 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

## 灵活属性 \[#flexible-attributes]

新增于 1.0.0

✅ 投资组合属性现在可部分甚至全部由用户数组计算。模拟后分析更加可控，例如覆盖部分模拟数据、不重跑完整组合即可测试超参数，或在关闭缓存时避免重复重建。

```python title="Compute the net exposure by caching its components"
>>> data = vbt.YFData.pull("BTC-USD")
>>> pf = vbt.PF.from_random_signals(data.close, n=100, seed=42)
>>> value = pf.get_value()  # (1)
>>> long_exposure = vbt.PF.get_gross_exposure(  # (2)
...     asset_value=pf.get_asset_value(direction="longonly"),
...     value=value,
...     wrapper=pf.wrapper
... )
>>> short_exposure = vbt.PF.get_gross_exposure(
...     asset_value=pf.get_asset_value(direction="shortonly"),
...     value=value,
...     wrapper=pf.wrapper
... )
>>> del value  # (3)
>>> net_exposure = vbt.PF.get_net_exposure(
...     long_exposure=long_exposure,
...     short_exposure=short_exposure,
...     wrapper=pf.wrapper
... )
>>> del long_exposure
>>> del short_exposure
>>> net_exposure
Date
2014-09-17 00:00:00+00:00    1.0
2014-09-18 00:00:00+00:00    1.0
2014-09-19 00:00:00+00:00    1.0
2014-09-20 00:00:00+00:00    1.0
2014-09-21 00:00:00+00:00    1.0
                             ...
2023-02-08 00:00:00+00:00    0.0
2023-02-09 00:00:00+00:00    0.0
2023-02-10 00:00:00+00:00    0.0
2023-02-11 00:00:00+00:00    0.0
2023-02-12 00:00:00+00:00    0.0
Freq: D, Length: 3071, dtype: float64
```

1.  调用实例方法，使用组合中存储的数据。
2.  调用类方法，显式提供全部数据。
3.  对象不再需要时立即删除，释放内存。

## 快捷属性 \[#shortcut-properties]

新增于 1.0.0

✅
[原地输出数组](/zh/features/backtesting/event-driven-backtesting/#in-place-outputs)可覆盖普通投资组合属性。组合自动采用预计算数组，之后所有计算都使用它，避免重复重建。

```python title="Modify the returns from within the simulation"
>>> data = vbt.YFData.pull("BTC-USD")
>>> size = data.symbol_wrapper.fill(np.nan)
>>> np.random.seed(42)
>>> rand_indices = np.random.choice(np.arange(len(size)), 10)
>>> size.iloc[rand_indices[0::2]] = -np.inf
>>> size.iloc[rand_indices[1::2]] = np.inf

>>> @njit
... def post_segment_func_nb(ctx):
...     for col in range(ctx.from_col, ctx.to_col):
...         return_now = ctx.last_return[col]
...         return_now = 0.5 * return_now if return_now > 0 else return_now
...         ctx.in_outputs.returns[ctx.i, col] = return_now

>>> pf = vbt.PF.from_def_order_func(
...     data.close,
...     size=size,
...     size_type="targetpercent",
...     post_segment_func_nb=post_segment_func_nb,
...     in_outputs=dict(
...         returns=vbt.RepEval("np.empty_like(close)")
...     )
... )

>>> pf.returns  # (1)
Date
2014-09-17 00:00:00+00:00    0.000000
2014-09-18 00:00:00+00:00    0.000000
2014-09-19 00:00:00+00:00    0.000000
2014-09-20 00:00:00+00:00    0.000000
2014-09-21 00:00:00+00:00    0.000000
                                  ...
2023-02-08 00:00:00+00:00   -0.015227
2023-02-09 00:00:00+00:00   -0.053320
2023-02-10 00:00:00+00:00   -0.008439
2023-02-11 00:00:00+00:00    0.005569  << modified
2023-02-12 00:00:00+00:00    0.001849  << modified
Freq: D, Length: 3071, dtype: float64

>>> pf.get_returns()  # (2)
Date
2014-09-17 00:00:00+00:00    0.000000
2014-09-18 00:00:00+00:00    0.000000
2014-09-19 00:00:00+00:00    0.000000
2014-09-20 00:00:00+00:00    0.000000
2014-09-21 00:00:00+00:00    0.000000
                                  ...
2023-02-08 00:00:00+00:00   -0.015227
2023-02-09 00:00:00+00:00   -0.053320
2023-02-10 00:00:00+00:00   -0.008439
2023-02-11 00:00:00+00:00    0.011138
2023-02-12 00:00:00+00:00    0.003697
Freq: D, Length: 3071, dtype: float64
```

1.  预计算收益率。
2.  实际收益率。

## 模拟区间 \[#simulation-ranges]

新增于 v2024.5.15

✅
每次模拟都有起止点，通常与数据首末行一致。可事先设置不同区间，现在也能在模拟期间调整，无需继续处理时即可停止。日期区间保存在组合对象中，所有度量和子图都能识别。只处理相关日期可加快执行，并为分析增添独立时间窗口这一维度
🔬

=== "示例 1：资金耗尽"
    ```python title="Simulate a quick liquidation scenario"
    >>> @njit
    ... def post_segment_func_nb(ctx):
    ...     value = vbt.pf_nb.get_group_value_nb(ctx, ctx.group)
    ...     if value <= 0:
    ...         vbt.pf_nb.stop_group_sim_nb(ctx, ctx.group)  # (1)

    >>> pf = vbt.PF.from_random_signals(
    ...     "BTC-USD",
    ...     n=10,
    ...     seed=42,
    ...     sim_start="auto",  # (2)
    ...     post_segment_func_nb=post_segment_func_nb,
    ...     leverage=10,
    ... )
    >>> pf.plot_value()  # (3)
    ```

    1.  当前组净值耗尽时停止处理。此回调停止模拟，不下达清算订单。
    2.  从首个信号开始模拟。
    3.  即使组合含完整原数据（2014 年至今），也确保全部度量与子图只用资金耗尽前的数据。

    固定随机种子的杠杆 BTC-USD 投资组合从开始至资金耗尽的净值. [Figure data (JSON)](/assets/figures/features/analysis/liquidation.abff6a63de05.json)

=== "示例 2：日期区间分析"
    ```python title="Analyze a date range of an already simulated portfolio"
    >>> pf = vbt.PF.from_random_signals("BTC-USD", n=10, seed=42)

    >>> pf.get_sharpe_ratio(sim_start="2023", sim_end="2024")  # (1)
    1.7846214408154346

    >>> pf.get_sharpe_ratio(sim_start="2023", sim_end="2024", rec_sim_range=True)  # (2)
    1.8377982089422782

    >>> pf.returns_stats(settings=dict(sim_start="2023", sim_end="2024"))  # (3)
    Start Index                  2023-01-01 00:00:00+00:00
    End Index                    2023-12-31 00:00:00+00:00
    Total Duration                       365 days 00:00:00
    Total Return [%]                             84.715081
    Benchmark Return [%]                        155.417419
    Annualized Return [%]                        84.715081
    Annualized Volatility [%]                     38.49976
    Max Drawdown [%]                             20.057773
    Max Drawdown Duration                102 days 00:00:00
    Sharpe Ratio                                  1.784621
    Calmar Ratio                                  4.223554
    Omega Ratio                                   1.378076
    Sortino Ratio                                 3.059933
    Skew                                          -0.39136
    Kurtosis                                     13.607937
    Tail Ratio                                    1.323376
    Common Sense Ratio                            1.823713
    Value at Risk                                -0.028314
    Alpha                                        -0.103145
    Beta                                          0.770428
    dtype: object
    ```

    1.  计算夏普比率时只考虑指定区间内的收益。注意，收益仍可能受区间外数据影响，例如未平仓头寸。
    2.  递归将日期区间应用到夏普比率依赖的全部度量，如净值、现金和订单，将区间外数据视为不存在。
    3.  确保全部统计一致使用该日期区间。

## 索引记录 \[#index-records]

新增于 1.12.0

✅ 如何回测“日期 Z 买入资产 Y 的 X
单位”等绑定时间与资产的指令？通常需要构建完整数组并手动设置每个细节。借助预处理器和重新设计的智能索引，现在可以用压缩记录格式提供全部信息！底层将记录数组转化为一组[索引字典](/zh/features/tooling/multidimensional-research/#index-dictionaries)，每个参数一个。

```python title="Define a basic signal strategy using records"
>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], missing_index="drop")
>>> records = [
...     dict(date="2022", symbol="BTC-USD", long_entry=True),  # (1)
...     dict(date="2022", symbol="ETH-USD", short_entry=True),
...     dict(row=-1, exit=True),
... ]
>>> pf = vbt.PF.from_signals(data, records=records)  # (2)
>>> pf.orders.readable
   Order Id   Column              Signal Index            Creation Index
0         0  BTC-USD 2022-01-01 00:00:00+00:00 2022-01-01 00:00:00+00:00  \
1         1  BTC-USD 2023-04-25 00:00:00+00:00 2023-04-25 00:00:00+00:00
2         0  ETH-USD 2022-01-01 00:00:00+00:00 2022-01-01 00:00:00+00:00
3         1  ETH-USD 2023-04-25 00:00:00+00:00 2023-04-25 00:00:00+00:00

                 Fill Index      Size         Price  Fees  Side    Type
0 2022-01-01 00:00:00+00:00  0.002097  47686.812500   0.0   Buy  Market  \
1 2023-04-25 00:00:00+00:00  0.002097  27534.675781   0.0  Sell  Market
2 2022-01-01 00:00:00+00:00  0.026527   3769.697021   0.0  Sell  Market
3 2023-04-25 00:00:00+00:00  0.026527   1834.759644   0.0   Buy  Market

  Stop Type
0      None
1      None
2      None
3      None
```

1.  支持每个可广播参数。行可用 "row"、"index"、"date" 或 "datetime" 指定，列可用 "col"、"column" 或 "symbol"
    指定。不提供行或列时，分别设置整行或整列；两者都不提供时，设置整个数组。行列可用整数位置、标签、日期时间，甚至复杂索引器表示！
2.  未用于记录的参数仍可照常提供；已用于记录的参数也可同时提供，作为默认值。
