点 Redis 8 来了 — 它是开源的

了解更多

使用原生 JSON 和查询功能在 Redis 中探索证券投资组合数据模型

本教程展示了如何优化券商应用程序,演示了如何利用 Redis 的 JSON 数据结构和增强的查询功能来完成任务。

在金融领域,券商的成功与其投资者使用其提供的交易应用程序的参与度息息相关。券商有动力提高投资者参与度,因为它会带来更多的管理资产、更多的交易以及(当然是券商最喜欢的部分)更多的费用和佣金。 

这些移动应用程序也很好地展示了通用系统设计,尤其是 Redis Enterprise 提供的技术。券商应用程序的关键在于时效性,这正是 Redis 作为实时数据平台的精髓所在,与文档存储(JSON)和快速查询功能相结合。因此,让我们将其作为一个案例研究,供服务金融服务受众的移动开发者参考。

一些券商应用程序设计用于处理任何时刻的特定数量的客户登录。当交易活动激增时,这些应用程序面临可伸缩性和性能挑战。这些技术故障和糟糕的客户体验对券商的收入、盈利能力和声誉都不利。

任何券商应用程序都有一长串技术必备项,包括高可用性、低延迟和快速响应时间、强一致性、可伸缩性、安全性和一致的性能。保证所有这些特性是一项挑战。软件开发者通常会混合使用技术和数据库,尽力在互操作性和公司预算问题上平衡所有这些参数。他们的应用程序环境包括 SQL 数据库、NoSQL 数据库和缓存,以提供实时的性能和可伸缩性表象。 

我们描述的券商应用程序需要一个内存数据库,该数据库可以确保高可用性、无缝可伸缩性和多样化的数据建模能力。Redis Enterprise 是一个支持 JSON、图和时间序列数据结构的内存数据库。使用独特的 Redis 索引和搜索功能进行查询可以满足券商应用程序的这些要求。Redis Enterprise 提供了许多功能来支持高可用性和可伸缩性,例如Active-Active 地理分布,这可以满足券商应用程序最苛刻的需求。    

在此,我们展示了一个使用 JSON 数据结构和 Redis 索引来存储和检索证券投资组合的券商应用程序示例实现。 

您可以参照示例代码来跟着做。

券商应用程序的数据模型如何工作

我们使用 JSON 对各种券商实体(图 1)进行了建模。每位投资者的个人详细信息都建模为一个名为 investor 的 JSON 文档。此文档存储了诸如投资者的法定姓名、地址、出生日期、社会安全号码(美国)或 Aadhar(印度)卡号以及纳税人识别号(美国为 TIN,印度为 PAN)等信息。

(尽管每位投资者在一家券商中可以拥有多个账户,但在此示例实现中,我们将每位投资者建模为拥有一个单一账户。)

图 1:证券投资组合数据模型

sample implementation diagram

每个 JSON 文档都有一个对应的键,其格式如下

trading:investor:<investorId>

一个示例文档 investor JSON 文档看起来像这样

{
      "id": "INV10001",
      "name": "John M.",
      "dob":"01-01-1980",
      "Address":"100 North Street"
      "uid": "35178235834",
      "pan": "AHUIOHO684"
}

应用程序可以扩展数据模型以容纳投资者拥有多个账户的情况。 

每个账户在 Redis 中都建模为一个 JSON 文档。账户文档还可以捕获重要数据,例如期权交易的批准级别以及账户是否允许进行保证金交易。

Account JSON 文档的键格式看起来像这样

trading:account:<accountNo>

相应的 account JSON 文档是

{
  "id": "ACC10001",
  "investorId": "INV10001",
  "accountNo": "ACC10001",
  "accountOpenDate": "01-01-2018",
  "accountCloseDate": "NA",
  "retailInvestor": true
}

每个账户在其投资组合中可以拥有数十种资产。这些资产可以是股票、债券、共同基金、交易型开放式指数基金 (ETF) 和期权。

每项资产都有独特的 JSON 文档数据元素。例如,账户中投资者拥有的股票条目将包括支付的价格、购买日期和数量。相比之下,账户购买的看涨期权需要行权价格、到期日和交易类型(卖出或买入期权),以及购买日期、支付的价格和购买的期权数量(数量)。 

典型投资者在给定日期和时间分批购买股票。因此,批次对于账户、证券和购买时间是唯一的。当股息作为股票支付时,投资者账户也可能被分配新的证券批次。例如,投资者可能会在股息再投资计划 (DRIP) 中注册其券商账户或个别证券。当公司支付股息时,券商会为 DRIP 中注册的每只股票创建新的证券批次。这只是一个示例。券商可以在许多场景下创建新的证券批次,例如公司分拆、并购和自动投资计划。    

每种此类证券批次的键格式如下

trading:securitylot:<accountNo>:<securityLotId>

一个示例文档 security lot JSON 文档看起来像这样

{
  "id": "SC61239693",
  "accountNo": "ACC10001",
  "ticker": "RDB", 
  "purchasedate": 1665082800,
  "price": 14500.00,
  "quantity": 10, 
  "type": "EQUITY"     	
}

在 Redis 中,批次中证券的购买日期以 Unix 日期/时间格式存储。  

最后,我们将每只已交易证券的详细信息存储在 JSON 中, 其键看起来像这样

trading:stock:<stockId>

相应的股票可以包含以下 JSON 元素

{
      "id": "NSE623846333",
      "companyname": "RDBBANK",
      "isin": "INE211111034",
      "stockName": "RDB",
      "description": "Something about RDB bank",
      "dateOfListing": "08-11-1995",
      "active": true
}

这种数据模型为券商处理数百万客户账户奠定了基础。

设置 Redis 以使用 Python 或 Java API

一切都已设置就绪。让我们在 Redis 上编写一些查询,以实现检索 JSON 对象和全文功能的几个场景。 

为此,我们需要使用我们选择的编程语言在 Redis 中添加数据。Redis 中的 JSON 数据结构受到官方 Redis 客户端的支持。例如,对于 Python,我们有 redis-py;对于 Java,我们有 Jedis。对于访问 Spring 中的企业模块,我们可以使用 Redis OM Spring 库。 

以下是使用 Python Redis API 创建文档的代码片段

# Python
import redis
connection = redis.Redis(host="127.0.0.1", port=6379)
account = {
  "id": "ACC10001", "investorId": "INV10001",
  "accountNo": "ACC10001", "accountOpenDate": "01-01-2018",
  "accountCloseDate": "NA", "retailInvestor": True
}
connection.json().set("trading:account:ACC10001", "$", account)

使用 Java 和 Jedis 库的相应代码如下

/* Java */
class Account {
    private String id;
    private String investorId;
    private String accountNo;
    private LocalDate accountOpenDate;
    private LocalDate accountCloseDate;
    private boolean retailInvestor;

    public Account(String id, String investorId, String accountNo, LocalDate openDate, LocalDate closeDate, boolean retailInvestor) {
        this.id = id;
        this.investorId = investorId;
        this.accountNo = accountNo;
        this.accountOpenDate = openDate;
        this.accountCloseDate = closeDate;
        this.retailInvestor = retailInvestor;
    }

    //...
}
//...
UnifiedJedis client = new UnifiedJedis(new HostAndPort("localhost", 6379));
Account firstAccount = new Account(
        "ACC10001", "INV10001", "ACC10001",
        LocalDate.of(2018, 1 , 1), null, true);

client.jsonSetWithEscape("trading:account:ACC10001", firstAccount);

使用索引和搜索功能查询证券投资组合

想象一下,一位散户投资者想查看他们持有的特定证券或一部分证券。这是一个典型的场景。在交易高峰时段,底层数据平台必须并发处理数百万账户的这些查询。

每个查询都应实时返回结果,以便底层应用程序提供一致的性能和用户体验。这可以通过 Redis 的一项功能来实现,该功能支持对 Redis 进行查询、二级索引和全文搜索。为此,我们首先需要在 JSON 文档上创建合适的二级索引。

在 Redis 中,索引具有独特的跟踪数据写入路径的能力,因此一旦您使用 FT.CREATE 创建索引并定义如何使用 SCHEMA 映射 JSON 文档,二级索引就会被填充。新的数据到来,索引会更新,您可以立即查询新的文档。

以下是 account 文档的 RediSearch 索引

FT.CREATE idx_trading_account 
   ON JSON 
      PREFIX 1 "trading:account:"
   SCHEMA 
      $.accountNo AS accountNo TEXT NOSTEM
      $.retailInvestor as retailInvestor TAG      
      $.accountOpenDate as accountOpenDate TEXT

以下是 security_lot 文档的索引

FT.CREATE idx_trading_security_lot 
   ON JSON 
      PREFIX 1 "trading:securitylot:" 
   SCHEMA 
      $.accountNo AS accountNo TEXT 
      $.ticker AS ticker TAG 
      $.price AS price NUMERIC SORTABLE 
      $.quantity AS quantity NUMERIC SORTABLE
      $.purchaseDate AS purchaseDate NUMERIC SORTABLE

我们在证券批次 (idx_trading_security_lot) 和账户 (idx_trading_account) 的文档上创建了索引。这些索引可以通过不同的方式进行查询,以满足投资者的实时、毫秒级需求。  

让我们为几个场景构建查询

让我们为几个场景构建查询

  • 按账户号码/ID 获取所有证券批次
  • 按账户号码/ID 和股票代码获取所有证券批次
  • 获取投资者证券投资组合中所有证券的总数量
  • 获取投资者在特定时间点证券投资组合中所有证券的总数量
  • 获取在给定日期和时间持有的证券的平均成本价格。证券的平均成本价格与当前价格结合使用时,可以提供该证券的收益或损失信息。

账户持有的证券批次

检索账户持有的所有证券批次可能是显示投资组合的最简单查询。该查询看起来像这样

FT.SEARCH idx_trading_security_lot '@accountNo:(ACC10001)'

其输出如下所示

127.0.0.1:6379> FT.SEARCH idx_trading_security_lot '@accountNo:(ACC10001)'
 1) (integer) 172
 2) "trading:securitylot:ACC10001:TEGO12981200447"
 3) 1) "$"
    2) "{\"id\":\"TEGO12981200447\",\"accountNo\":\"ACC10001\",\"ticker\":\"RDBMOTORS\",\"date\":1648578600,\"price\":43845.0,\"quantity\":66,\"type\":\"EQUITY\"}"
 4) "trading:securitylot:ACC10001:UHZW18076572669"
 5) 1) "$"
    2) "{\"id\":\"UHZW18076572669\",\"accountNo\":\"ACC10001\",\"ticker\":\"RDBFOODS\",\"date\":1642012200,\"price\":1975000.0,\"quantity\":55,\"type\":\"EQUITY\"}"
 6) "trading:securitylot:ACC10001:QHSL13846265328"
 7) 1) "$"
    2) "{\"id\":\"QHSL13846265328\",\"accountNo\":\"ACC10001\",\"ticker\":\"RDBMOTORS\",\"date\":1647369000,\"price\":42705.0,\"quantity\":23,\"type\":\"EQUITY\"}"
  .
  .

检索账户中特定证券的所有批次

投资者可以轻松过滤其投资组合视图,并查看其在特定证券中的头寸。

FT.SEARCH idx_trading_security_lot '@accountNo:(ACC10001) @ticker:{RDBMOTORS}'

上述命令的输出看起来像这样

127.0.0.1:6379> FT.SEARCH idx_trading_security_lot '@accountNo: (ACC10001) @ticker:{RDBMOTORS}'
 1) (integer) 90
 2) "trading:securitylot:ACC10001:TEGO12981200447"
 3) 1) "$"
    2) "{\"id\":\"TEGO12981200447\",\"accountNo\":\"ACC10001\",\"ticker\":\"RDBMOTORS\",\"date\":1648578600,\"price\":43845.0,\"quantity\":66,\"type\":\"EQUITY\"}"
 4) "trading:securitylot:ACC10001:QHSL13846265328"
 5) 1) "$"
    2) "{\"id\":\"QHSL13846265328\",\"accountNo\":\"ACC10001\",\"ticker\":\"RDBMOTORS\",\"date\":1647369000,\"price\":42705.0,\"quantity\":23,\"type\":\"EQUITY\"}"
 6) "trading:securitylot:ACC10001:TIMW18620419852"
 7) 1) "$"
    2) "{\"id\":\"TIMW18620419852\",\"accountNo\":\"ACC10001\",\"ticker\":\"RDBMOTORS\",\"date\":1641321000,\"price\":48695.0,\"quantity\":30,\"type\":\"EQUITY\"}"
  .
  .

账户中每只证券的总数量

目前,它专注于检索与查询谓词和需求匹配的特定键。Redis 的查询功能不仅限于此。您可以使用 FT.AGGREGATE 进行分组、排序、过滤和进行算术运算 SUM。应用程序可以使用类似于以下的聚合查询获取证券的总数量或支付的价格

FT.AGGREGATE idx_trading_security_lot '@accountNo: (ACC10001)' GROUPBY 1 @ticker REDUCE SUM 1 @quantity as totalQuantity

其输出看起来像

127.0.0.1:6379> FT.AGGREGATE idx_trading_security_lot '@accountNo: (ACC10001)' GROUPBY 1 @ticker REDUCE SUM 1 @quantity as totalQuantity
1) (integer) 2
2) 1) "ticker"
   2) "RDBMOTORS"
   3) "totalQuantity"
   4) "4502"
3) 1) "ticker"
   2) "RDBFOODS"
   3) "totalQuantity"
   4) "4581"

账户在特定日期和时间持有的证券

这样的查询可以帮助投资者了解在特定时间点(例如月末或年末)持有的证券总数量。

FT.AGGREGATE idx_trading_security_lot '@accountNo: (ACC10001) @date:[0 1665082800]' GROUPBY 1 @ticker REDUCE SUM 1 @quantity as totalQuantity

预期输出看起来像这样

127.0.0.1:6379> FT.AGGREGATE idx_trading_security_lot '@accountNo: (ACC10001) @date:[0 1665082800]' GROUPBY 1 @ticker REDUCE SUM 1 @quantity as totalQuantity
1) (integer) 2
2) 1) "ticker"
   2) "RDBMOTORS"
   3) "totalQuantity"
   4) "4502"
3) 1) "ticker"
   2) "RDBFOODS"
   3) "totalQuantity"
   4) "4581"

账户在给定时刻持有的每只证券的平均购买价格

要查找账户在特定日期和时间持有的每只股票的平均成本价格,我们首先计算账户中持有的所有证券的批次价值。然后我们汇总这些证券的总数量。最后,我们计算这些证券的平均成本价格。

FT.AGGREGATE idx_trading_security_lot '@accountNo:(ACC10001) @date:[0 1665498506]' apply '(@price * @quantity)' as lotValue groupby 1 @ticker reduce sum 1 @lotValue as totalLotValue reduce sum 1 @quantity as totalQuantity apply '(@totalLotValue/(@totalQuantity*100))' as avgPrice

上述命令的输出如下

127.0.0.1:6379> FT.AGGREGATE idx_trading_security_lot '@accountNo:(ACC10001) @date:[0 1665498506]' apply '(@price * @quantity)' as lotValue groupby 1 @ticker reduce sum 1 @lotValue as totalLotValue reduce sum 1 @quantity as totalQuantity apply '(@totalLotValue/(@totalQuantity*100))' as avgPrice
1) (integer) 2
2) 1) "ticker"
   2) "RDBMOTORS"
   3) "totalLotValue"
   4) "205251865"
   5) "totalQuantity"
   6) "4502"
   7) "avgPrice"
   8) "455.912627721"
3) 1) "ticker"
   2) "RDBFOODS"
   3) "totalLotValue"
   4) "8496437015"
   5) "totalQuantity"
   6) "4581"
   7) "avgPrice"
   8) "18547.1229317"

成果展示 

我们希望这个示例能激起您对这些 Redis Enterprise 功能的兴趣,并鼓励您进一步了解它们。

Redis 文档存储功能提供了对 JSON 的完全支持,以及用于操作 JSON 元素的 JSONPath 语法、快速的数据访问以及对 JSON 值的原子操作。多项基准测试表明,RedisJSON 在延迟和读写吞吐量等指标上的表现优于其竞争对手。

Redis 上的查询和搜索功能允许您快速创建对 HASH 和 JSON 文档的索引,并使用实时索引即时查询文档。这些索引让您可以闪电般的速度查询数据,执行复杂的聚合,并按属性、数字范围和地理距离进行过滤。

这只是一个开始

想了解更多?您可以选择以下方法之一来安装和开始使用 Redis Stack: