ArcticDB 中的时间旅行

alex-owens

Alex Owens

2024年4月12日

polar bear with suitcase

之前的博文讨论了存储在ArcticDB中数据的不可变性,将其作为优化性能、提高横向扩展能力和防止数据损坏的一种手段。这种不可变性的另一个结果是能够“时间旅行”回滚到符号的早期版本。在存储的符号具有日期时间索引的使用场景中,这也被称为“双时间性”,因为你不仅可以获取最新版本符号中特定日期范围内的数据,还可以获取历史版本在特定时间点的数据。

那么为什么这很有用呢?让我们通过一个真实的玩具示例来展示这个功能为何如此强大。

假设你正在从一个数据供应商那里接收数据,他们每天提供 CSV 文件,其中包含该交易日内所有证券每分钟实时交易的开盘价、收盘价、最高价、最低价、交易量和交易数据

with open("AAPL_2024-01-01.csv", "r") as f:
    daily_df = pandas.read_csv(f, index_col="datetime")
lib.append("AAPL", daily_df)
print(lib.read("AAPL").data)                         open     close       low      high    volume  trades
datetime                                                                     
2024-01-01 08:00:00  0.758089  0.339878  0.781040  0.098037  0.134402     346
2024-01-01 16:29:00  0.221275  0.773076  0.321803  0.586879  0.250758     201

这里我只包含了当天的第一分钟和最后一分钟,以减少数据量,并且数值是随机生成的。对于每个证券,每天的一般流程如上所示。然而,数据供应商有时也会重新公布早期时间段的数据,以纠正原始数据中的错误。假设我们有 2024 年前 3 天的标准每日数据

                         open     close       low      high    volume  trades
datetime                                                                     
2024-01-01 08:00:00  0.758089  0.339878  0.781040  0.098037  0.134402     346
2024-01-01 16:29:00  0.221275  0.773076  0.321803  0.586879  0.250758     201
2024-01-02 08:00:00  0.543514  0.121713  0.908510  0.859530  0.673515     810
2024-01-02 16:29:00  0.363511  0.506118  0.445974  0.031269  0.743444     665
2024-01-03 08:00:00  0.054882  0.319868  0.520630  0.986617  0.812310     847
2024-01-03 16:29:00  0.237748  0.932168  0.990305  0.026718  0.109527     527

然后,在第四天,供应商提供通常的每日数据,并重新公布 2024–01–02 的数据。将数据写入ArcticDB现在将包含如下代码

with open("AAPL_2024-01-02_restatement.csv", "r") as f:
    restated_df = pandas.read_csv(f, index_col="datetime")
lib.update("AAPL",  restated_df)
print(lib.read("AAPL").data)                         open     close       low      high    volume  trades
datetime                                                                     
2024-01-01 08:00:00  0.758089  0.339878  0.781040  0.098037  0.134402     346
2024-01-01 16:29:00  0.221275  0.773076  0.321803  0.586879  0.250758     201
2024-01-02 08:00:00  0.726245  0.209358  0.382218  0.652552  0.060542     452
2024-01-02 16:29:00  0.912180  0.317210  0.460713  0.581922  0.330218     642
2024-01-03 08:00:00  0.054882  0.319868  0.520630  0.986617  0.812310     847
2024-01-03 16:29:00  0.237748  0.932168  0.990305  0.026718  0.109527     527
2024-01-04 08:00:00  0.813585  0.571894  0.813374  0.598003  0.542102     184
2024-01-04 16:29:00  0.808495  0.375533  0.665888  0.847430  0.885961     245

其中第三行和第四行现在与之前不同。我们现在遇到了一个问题。假设我们决定在 2024–01–03 进行交易,依据的是当时我们可获得的信息。如果我们只能读取符号的最新版本

AAPL
,那么 2024-01-02 的价格数据将与我们做出决定时的数据不符。这时,“时间旅行”就派上了用场。这个
as_of
参数可用于
read
用于指定我们要读取的是早期版本,而不仅仅是最新版本。在此示例中

print(lib.read("AAPL", as_of=2).data)

                         open     close       low      high    volume  trades
datetime                                                                     
2024-01-01 08:00:00  0.758089  0.339878  0.781040  0.098037  0.134402     346
2024-01-01 16:29:00  0.221275  0.773076  0.321803  0.586879  0.250758     201
2024-01-02 08:00:00  0.543514  0.121713  0.908510  0.859530  0.673515     810
2024-01-02 16:29:00  0.363511  0.506118  0.445974  0.031269  0.743444     665
2024-01-03 08:00:00  0.054882  0.319868  0.520630  0.986617  0.812310     847
2024-01-03 16:29:00  0.237748  0.932168  0.990305  0.026718  0.109527     527

将准确显示 2024–01–03 追加操作完成后数据看起来的样子。这引出了一个问题,我们如何知道应该读取哪个版本?

有三种选择。所有修改操作

write/append/update
返回一个
VersionedItem
对象。除其他属性外,此对象还有一个
version
属性,告知你刚刚创建的符号的版本号。这些信息可以存储在一个单独的符号中,也可以存储在完全不同的数据库中,具体取决于实际需求。

另外,可以向

as_of
提供一个时间戳(UTC 时间),它将检索在指定时间点是最新的版本。例如,如果供应商每天下午 5 点提供数据,并且我们总是在下午 6 点之前将其写入 ArcticDB,那么

lib.read("AAPL", as_of=pandas.Timestamp("2024-01-03T18:00:00"))

将检索与请求版本 2 相同的数据。

最后,我们可以使用快照来定义在特定时间点精确的符号-版本对集合。然后可以将快照名称提供给该

as_of
参数,以检索该快照引用的符号版本

lib.read("AAPL", as_of="snapshot-2024-01-03")

在这里给出的这个简单示例中,使用快照并没有比使用时间戳带来任何好处。然而,在符号持续修改且交易决策基于多个符号而非单个符号的情况下,快照提供了一种精确引用符号-版本对集合的方式,无需担心时钟偏差等细节问题。

但是,如果你不需要这个功能,并且只关心最新版本怎么办?在这种情况下,保留永远不会再次读取的旧数据会浪费磁盘空间。在这种情况下,所有修改操作都有一个参数

prune_previous_versions
在写入新版本时删除符号的旧版本,但快照引用的任何版本除外。