# 记录与映射数组 (/zh/features/analytics/records-and-mapped-arrays)

订单、交易、回撤和形态匹配属于事件，而非时间序列：大多数 K 线没有事件，有些则同时有多个。VBT
将这类稀疏事件和区间存储为带列映射的记录，让您无需为每个字段创建稠密数组，就能跨数千列筛选、归约、重新分组和绘图。

```python title="Find the order with the worst fill among 20,000 orders in 1,000 columns"
>>> close = vbt.GBMOHLCData.pull("S", start="2024-01-01", end="2025-01-01", seed=42).close
>>> np.random.seed(42)
>>> slippage = pd.DataFrame(
...     np.random.uniform(0, 0.005, size=(len(close), 1000)),
...     index=close.index,
... )
>>> pf = vbt.PF.from_random_signals(close, n=10, seed=42, slippage=slippage)  # (1)
>>> orders = pf.orders
>>> print(orders.count().sum())
20000

>>> fill_vs_close = orders.price.values / close.values[orders.idx.values] - 1
>>> cost = orders.map_array(  # (2)
...     np.where(orders.side.values == 0, fill_vs_close, -fill_vs_close)
... )
>>> cost.max().describe().round(4)  # (3)
count    1000.0000
mean        0.0048
std         0.0002
min         0.0035
25%         0.0047
50%         0.0048
75%         0.0049
max         0.0050
Name: max, dtype: float64

>>> readable = cost.to_readable()
>>> readable.loc[readable["Value"].idxmax()]
Id                                1
Column                          165
Index     2024-03-12 00:00:00+00:00
Value                      0.004999
Name: 3301, dtype: object
```

1.  一条合成价格、1,000 列随机滑点，以及每列 10 次随机入场和退出。
2.  每个订单一个值：成交价比收盘价差多少。
3.  将 20,000 个值归约为每列的最差成交，再汇总 1,000 个结果。

两万个订单占用两万行，而不是 366 根 K 线乘以 1,000 列再乘以每个字段。最差成交是第 165 列的第二个订单，发生于 2024 年 3 月 12 日。

## 记录 \[#records]

记录数组是结构化 NumPy 数组，每个事件一行，具有列、K
线、价格和规模等字段。记录类将其与数据的时间索引和列标签一起封装，让同一对象同时了解原始位置及其对应日期。订单、日志、交易、持仓、回撤和形态匹配都是记录。

区间是带有起点和终点的记录。任意布尔掩码都能转换为区间，将时段变成可衡量的对象：

```python title="Turn overbought periods into ranges"
>>> btc = vbt.YFData.pull("BTC-USD", start="2020-01-01", end="2025-01-01")
>>> rsi = btc.run("rsi", window=14).rsi.rename("RSI")
>>> overbought = vbt.Ranges.from_array(rsi > 70)
>>> print(overbought.count(), overbought.coverage.round(3))  # (1)
42 0.128

>>> longest = overbought.apply_mask(overbought.duration.top_n_mask(3))
>>> longest.readable[["Start Index", "End Index", "Status"]]
                Start Index                 End Index  Status
0 2023-01-11 00:00:00+00:00 2023-01-30 00:00:00+00:00  Closed
1 2023-10-20 00:00:00+00:00 2023-11-14 00:00:00+00:00  Closed
2 2024-11-06 00:00:00+00:00 2024-11-25 00:00:00+00:00  Closed
>>> longest.duration.values
array([19, 25, 19])
```

1.  42 个独立超买时段，覆盖 12.8% 的 K 线。

记录也可根据您自己的数据构建。带有日期和交易标的的 DataFrame
或字典列表，可以转换为具有类型的记录；[索引记录](/zh/features/backtesting/backtesting-engine/#index-records)正是通过这种方式将稀疏订单传入模拟。

## 映射数组 \[#mapped-arrays]

映射数组为每条记录保存一个值，同时保留记录的列和 K 线位置。记录对象的每个字段都可作为映射数组访问，例如 `pf.trades.pnl` 或
`pf.drawdowns.duration`；`map_array` 则可附加您计算的任意数值。映射数组支持通过 `mean`、`max`、`idxmax`、`count`
或任意编译函数按列或组归约，通过 `top_n_mask` 等掩码筛选，并在需要表格时用 `to_pd` 或 `to_readable` 转换为 pandas。`to_pd` 将每个值放到其记录对应的
K 线上。同一列的多条记录共享一根 K 线时，会保留最后一条并发出警告。传入 `reduce_func_nb`（例如 `"sum"`）可合并它们，传入 `repeat_index=True`
可全部保留，或用 `ignore_index=True` 将值无日期堆叠。

这让您能够针对回测提出更具体的问题：几乎没有浮盈的亏损交易持续了多久？哪些订单成本最高？先筛选记录或映射值，再对选定事件运行相同归约。结果保留资产和参数标签。[交易分析](/zh/features/analytics/trade-analytics/)展示了如何将此流程用于多空交易及其价格偏移。

## 索引与重新分组 \[#indexing-and-regrouping]

记录保持按列排序，列映射记住每列记录的起始位置，因此选择某列或分组（例如 `pf.trades["BTC-USD"]`）无需扫描全部记录。分组可在事后更改：同一组交易记录通过
`group_by=False` 按资产报告，也可通过其他分组方式按策略报告。如需跨列按时间查看，请按索引列对可读表格排序。

例如，先分别报告三个资产，再将 A 和 B 合并为一个研究分组：

```python title="Regroup trade profits without rerunning the backtest"
>>> group_close = pd.DataFrame({"A": [10.0, 11.0, 12.0], "B": [20.0, 18.0, 19.0], "C": [30.0, 33.0, 36.0]}, index=pd.date_range("2026-01-01", periods=3))
>>> group_pf = vbt.PF.from_orders(group_close, size=np.array([1, 0, -1])[:, None])
>>> trade_pnl = group_pf.trades.status_closed.pnl
>>> trade_pnl.sum()
A    2.0
B   -1.0
C    6.0
Name: sum, dtype: float64

>>> trade_pnl.sum(group_by=["Group 1", "Group 1", "Group 2"])
group
Group 1    1.0
Group 2    6.0
Name: sum, dtype: float64
```

同一组记录现在回答了不同的报告问题。您可以按行业、策略系列或每列的其他标签分组。如需让资产在模拟运行时共享资金，请使用[共享现金](/zh/features/backtesting/portfolio-accounting/#shared-cash)。

## 自定义记录类 \[#custom-record-classes]

您自己的事件可以有自己的类。定义包含所需字段的 NumPy dtype，附加标题和值映射，让可读表格显示名称而非代码，再继承 `vbt.Records` 或
`vbt.Ranges`，即可获得筛选、映射字段、统计和绘图功能。记录像其他 VBT 对象一样可以保存到磁盘并加载。在 Numba 代码中，用 `record["price"]`
这样的方括号读取记录字段，无论是否开启编译都能工作。

## 原生支持 OHLC 的类 \[#ohlc-native-classes]

新增于 1.3.0

✅ 此前 OHLC 数据用于模拟，但分析只使用收盘价。现在，大多数类都能跟踪全部 OHLC 数据，让定量和定性分析更加准确。

```python title="Plot trades of a random portfolio"
>>> data = vbt.YFData.pull("BTC-USD", start="2020-01", end="2020-03")
>>> pf = vbt.PF.from_random_signals(
...     open=data.open,
...     high=data.high,
...     low=data.low,
...     close=data.close,
...     n=10,
...     seed=42
... )
>>> pf.trades.plot().show()
```

BTC-USD OHLC 与随机投资组合交易的入场和退出. [Figure data (JSON)](/assets/figures/features/analysis/ohlc-native-classes.4ffa9efd631b.json)
