dot 速度的未来正在您的城市举办的活动中。

加入我们在Redis发布活动

使用Redis构建实时交易平台

投资组合是财富和资产管理行业的基石。自哈里·马科维茨开创了现代投资组合理论以来,资产和财富管理专业人士一直致力于在给定风险水平下最大化其投资组合的回报。如今,行业的专业人士加入了数百万散户投资者,他们彻底改变了投资格局。这些新进入者为零售经纪商、交易所和清算所的交易基础设施背后的技术带来了巨大影响。

以 2021 年 1 月的游戏驿站股票狂热为例。散户投资者开始以创纪录的水平交易游戏驿站股票。这些投资者还涌入其他“迷因股”,如 AMC 娱乐,导致市场整体波动率在几天内上涨了 76% 以上,以 VIX 为衡量标准。这种波动导致数千种证券的价格压力。数百万投资者同时疯狂地试图访问他们的投资组合,但面对无法满足需求的应用程序。投资者对应用程序在最需要的时候表现不佳的公司并不友好。

展示:2021 年 1 月散户投资者在游戏驿站狂热期间导致的市场波动率上升

在这些疯狂的时期,大多数投资者都在寻找关于其投资组合的两个数据点,他们需要始终可以访问这些数据点

  1. 当时投资组合的总价值是多少?
  2. 投资组合中特定证券的收益或损失是多少?

对这些问题的答案可以导致投资者买入、卖出或持有特定证券。在当今快速变化的市场中,任何延迟都可能意味着失去机会和利润。您需要实时访问价格才能回答这些问题 - 但存在两个重大挑战

  • 同时更新数千种证券的价格
  • 一次回答数百万客户请求。

证券的价格可能会根据交易量、特定证券的波动性和市场波动性快速变化。另一方面,一家经纪商可能拥有数百万客户,每个客户在其投资组合中都有几十种证券。一旦客户登录,他们的投资组合需要使用最新价格更新 - 并随着经纪商从交易所接收价格而保持更新。

本质上,我们正在创建一个实时股票图表。许多经纪商应用程序不会尝试大规模地执行此操作。相反,这些应用程序会拉取最新价格,而不是将价格推送到数百万客户。例如,他们的投资组合页面上可能有一个刷新按钮。

这些下一代挑战并非微不足道,也不能通过磁盘数据库轻松解决,磁盘数据库并非为每秒处理数百万次操作而设计。金融行业的需求需要一个能够轻松扩展并每秒处理数亿次操作的数据库。Redis 企业版,一个内存数据库平台,有可能解决这些众多挑战。

(来源:照片由Anna Nekrashevich 来自Pexels)

这是涵盖金融领域各种实时用例的系列博客的第一篇。我们将介绍每个用例的详细信息和业务挑战,以及 Redis 企业版在解决这些挑战方面可以发挥的作用。作为博客的一部分,我们提供样本设计、数据模型和代码样本。我们还将讨论每种方法的优缺点。

在本博文中,我们将涵盖以下内容

  • 在 Redis 企业版上构建高性能和可扩展的证券投资组合数据模型的样本实现。
  • 随着经纪商从交易所接收最新价格,实时更新投资组合中证券的价格。

一旦客户应用程序检索了投资组合并接收了最新价格,它就可以

  • 计算投资组合的总投资组合价值。
  • 计算每个投资组合持仓的收益或损失。

证券投资组合数据模型

让我们从对投资组合中的持仓进行建模开始。在下图中,CVS Health Corp.(纽约证券交易所:CVS)是我们示例持仓之一。有两个单独的 CVS 批次 - 第一个是在 2021 年 1 月 4 日获得的,第二个是在 2021 年 3 月 1 日获得的。每个批次在买入交易期间购买了相同数量的股票。两次交易都是 10 股,但每股价格不同 - 第一个批次为 68.3378 美元,第二个批次为 68.82 美元。投资组合中 CVS 持仓的总数量为 20,平均成本计算如下:((68.3378 * 10)+(68.82 * 10))/20 = 每股 68.5789 美元。

展示:证券投资组合中持仓的描述(来源:E*Trade,作者注释)

实现需求

Redis 的数据表示是扁平的 - 例如,不能在另一个集合内嵌入集合。因此,ER 图描述的数据模型不一定可以直接实现。直接实现实体模型可能没有预期的性能特征,因此在实现时需要进行不同的思考。在本节中,我们将介绍使用 Redis 设计高性能和可扩展实现时所需的一些基本设计原则。

此数据模型提到了以下实体

ER 图提供了直观的表示,可以帮助人们了解情况。

上面的图表中缺少的是传入的价格集,尽管它们被记录在证券的价格历史中 - 以及瞬时价值和收益的计算,因为价格会发生变化。因此,ER 图表示了相对静态的数据,这些数据是执行投资组合估值的环境。

总体架构

关于此系统需要注意的关键点包括

  • 及时性(即延迟)至关重要。这是整体驱动力。
  • 投资组合价值的计算结合了特定于帐户的数据(即批次信息)和帐户之间共享的数据(即证券价格)。因此,特定于帐户的数据是使用共享数据的环境。
  • 仅需要为投资者在线的帐户(总帐户数的一小部分)计算投资组合价值。
  • 数据流经系统,从产生价格的交易所流向向在线投资者展示投资组合价值的客户端机器(浏览器、移动电话应用程序)。
  • 具有高度动态性的特定区域包括输入数据流速率和在线投资者数量。

鉴于这些要点,一些一般方法是

  • 依靠 Redis 的内存架构来实现对静态和动态数据的低延迟访问。
  • 通过使用 Redis 的数据结构来优化数据建模,以提供对缓慢变化的环境数据的快速访问。
  • 使用 Redis 的通信结构(流、消费者组、发布/订阅)来处理动态数据需求。
  • 将数据存储减少到仅必需的内容,而不会影响整体系统性能。
  • 在客户端本身实现特定于客户端的计算。这会随着在线投资者数量的增加而自然且自动地扩展,极大地减轻了规模负担。

以下是主要计算组件和数据流

请注意,Redis 企业版包含一个或多个节点,分布在许多机器上 - 部署在本地、Kubernetes、混合云部署、托管服务或本机第一方云服务上 - 并且将有数十万在线投资者及其选择的客户端。

Redis 企业版组件

安全价格更新将被 Redis 流吸收。证券的更新将混合在一起,需要进行分解以使数据变得有用。消费者组将用于执行此分解,并将数据处理成两种结构,每种证券一个

  • RedisTimeSeries 数据库,用于跟踪价格变化的历史记录(以及记录任何刚连接客户端的最新价格)
  • 发布/订阅代理通道,将价格变化通知推送到已订阅该通道的客户端(即投资组合包含该证券的投资者)

下图详细说明了架构的这一部分

我们模型中最重要的因素是特定于帐户的数据,这些数据代表批次和相关的安全性。我们将比较两种实现方式,作为思考如何在 Redis 中建模数据的示例,重点关注性能。其他实现方式也是可能的——我们的目标是介绍在 Redis 中实现数据时的总体设计原则和思考过程。

我们将使用以下信息作为具体示例

我们以最低的货币单位进行定价,以避免使用浮点数,并保持所有内容为整数。我们可以让客户端处理转换为美元和美分的转换。在本例中,我们使用精确到小数点后两位的价格。

数据模型 A

我们的第一个实现使用 SET 记录了帐户中所有批次的 ID,这些 ID 由帐户 ID 标识,然后为每个 LOT 使用一个 Redis HASH,由 LOT ID 标识,以代码、数量和购买价格作为字段。换句话说,我们使用 HASH 来模拟 LOT 实体结构,LOT 实体的每个属性都是 Redis HASH 中的字段

使用这种数据模型,我们每个帐户都有一个键,并且一个值包含存储为 Redis SET 的该帐户的所有 LOT ID

lotids:<ACCOUNT_ID> SET <LOTID>

此外,对于每个 lotid,我们将有一个 HASH,其字段是代码、数量和购买价格

lot:<LOTID> HASH <ticker TICKER> <quantity INTEGER> <price INTEGER>

具体来说,我们将这样创建键

127.0.0.1:6379> SADD lotids:ACC-1001 LOT-9001 LOT-9002
(integer) 2
127.0.0.1:6379> HMSET lot:LOT-9001 ticker AAPL quantity 200 price 12556
OK
127.0.0.1:6379> HMSET lot:LOT-9002 ticker CAT quantity 1200 price 18063
OK

RedisTimeSeries 模块允许存储和检索相关的时间和值对,以及高容量插入和低延迟读取。我们将获取客户端在使用相应时间序列键时访问的感兴趣代码的最新价格

price_history:<TICKER> TIMESERIES <price INTEGER>

127.0.0.1:6379> TS.GET price_history:APPL
1) (integer) 1619456853061
2) 12572
127.0.0.1:6379> TS.GET price_history:CAT
1) (integer) 1619456854120
2) 18021

并订阅定价频道以获取更新

<TICKER> SUBSCRIPTION_CHANNEL

要获取所有数据,客户端将执行以下操作

  1. 1 次 SMEMBERS 在 lotids 键上——时间复杂度 O(N),其中 N 是批次数量
  2. N 次 HGETALL 在 lot 键上——时间复杂度 N 次 O(1)
  3. T 次 TS.GET 在 price_history 键上——时间复杂度 T 次 O(1),其中 T 是代码数量
  4. 1 次 SUBSCRIBE 在 <TICKER> 频道上——时间复杂度 O(T)(可以在一次对 SUBSCRIBE 的调用中订阅所有频道)

总体时间复杂度为 O(N +T)。

具体来说,操作一和二将是

127.0.0.1:6379> SMEMBERS lotids:ACC1001
1) "LOT-9001"
2) "LOT-9002"
127.0.0.1:6379> HGETALL lot:LOT-9001
1) "ticker"
2) "AAPL"
3) "quantity"
4) "200"
5) "price"
6) "12556"
127.0.0.1:6379> HGETALL lot:LOT-9002
1) "ticker"
2) "CAT"
3) "quantity"
4) "1200"
5) "price"
6) "18063"

我们可以通过使用 管道化 (客户端端的一种批处理形式)和/或重复使用 LUA 脚本(使用 SCRIPT LOAD & EVALSHA)来最小化网络延迟。旁注: 事务 可以使用管道实现,可以减少网络延迟,但这特定于客户端,其目标是在服务器上实现原子性,因此它们并没有真正解决网络延迟问题。管道包含命令,这些命令的输入和输出必须彼此独立。LUA 脚本要求所有键都提前提供,并且所有键都必须散列到同一个槽(有关更多详细信息,请参阅 Redis Enterprise 文档 关于此主题的说明)。

鉴于这些约束,我们可以看到操作到管道的分配是

  • 管道 1:操作 #1 的单个命令
  • 管道 2:操作 #2 的所有 N 个命令
  • 管道 3:操作 #3 和 #4 的所有 N 个命令

并且使用 LUA 脚本是不可能的,因为每个操作使用不同的键,并且这些键没有可以散列到同一个槽的公共部分。

在利用这种模型时,我们的时间复杂度为 O(N+T),并且有三个网络跳跃。

数据模型 B

另一种模型是扁平化 LOT 实体结构,并使用由帐户 ID 标识的键来表示每个实体属性——每个属性(数量、代码、价格)一个这样的键。每个 HASH 中的字段将是 LOT ID 和与数量、代码或价格相对应的值。因此,我们将有键

tickers_by_lot: <ACCOUNT_ID> HASH <LOTID TICKER>

quantities_by_lot:<ACCOUNT_ID> HASH <LOTID INTEGER>

prices_by_lot:<ACCOUNT_ID> HASH <LOTID INTEGER>

这些哈希将替换来自数据模型 A 的 LOTID 和 LOT 键,而 price_history 和 <TICKER> 键将保持不变。

创建键

HSET tickers_by_lot:ACC-1001 LOT-9001 AAPL LOT-9002 CAT
HSET quantities_by_lot:ACC-1001 LOT-9001 200 LOT-9002 1200
HSET prices_by_lot:ACC-1001 LOT-9001 125.56 LOT-9002 180.63

检索值

127.0.0.1:6379> HGETALL tickers_by_lot:ACC-1001
1) "LOT-9001"
2) "AAPL"
3) "LOT-9002"
4) "CAT"
127.0.0.1:6379> HGETALL quantities_by_lot:ACC-1001
1) "LOT-9001"
2) "200"
3) "LOT-9002"
4) "1200"
127.0.0.1:6379> HGETALL prices_by_lot:ACC-1001
1) "LOT-9001"
2) "12556"
3) "LOT-9002"
4) "18063"

客户端现在需要执行的操作将是

  1. 1 次 HGETALL 在 lot_quantity 键上——时间复杂度 N x O(1)
  2. 1 次 HGETALL 在 lot_ticker 键上——时间复杂度 N x O(1)
  3. 1 次 HGETALL 在 lot_price 键上——时间复杂度 N x O(1)
  4. T 次 TS.GET 在 price_history 键上——时间复杂度 T x O(1),其中 T 是代码数量
  5. 1 次 SUBSCRIBE 在 <TICKER> 频道上——时间复杂度 1 x O(T)

这具有 O(N+T) 的总体时间复杂度——与之前相同。

从管道的角度来看,这将变成

  • 管道一——操作 #1、#2 和 #3 的所有命令
  • 管道二——操作 #4 和 #5 的所有 T 个命令

因此,我们减少了一个网络跳跃——在绝对意义上并不多,但在相对意义上减少了 33%。

此外,我们可以轻松使用 LUA,因为我们知道键,并且可以将任何特定帐户的所有键映射到同一个槽。鉴于操作的简单性,我们不会进一步深入 LUA,但请注意,这种设计至少是可能的!

在一个简单的基准测试中,数据模型 B 运行速度快了 4.13 毫秒(在数千次运行中进行基准测试)。考虑到这在客户端为帐户初始化时只运行一次,这可能不会影响整体性能。

总结

在本博文中,我们展示了使用 Redis 数据类型实现实体模型的两种可能实现方式。我们还介绍了在选择 Redis 数据类型时应执行的时间复杂度分析,以及对网络性能改进的考虑——当需要大规模和高性能时,这是一个关键步骤。在随后的博文中,我们将随着数据模型的扩展进一步扩展这些想法。

我们介绍了在规模化管理证券投资组合方面的一些业务挑战,并展示了以下内容

  • 一个用于实现实时和可扩展证券投资组合的 Redis 数据模型。
  • 一个高性能、实时价格更新系统,可用于计算投资组合的总价值以及每个持仓的收益或亏损。

有了这两个关键功能,经纪应用程序客户端可以提供实时投资组合更新,这些更新的性能和可扩展性足以处理数百万个帐户。这种设计可以实时显示投资组合的总价值以及每个持仓的收益或亏损。这种数据模型和架构也可以应用于除证券之外的用例,以涵盖加密货币、广告交易等。