dot 快速未来的浪潮即将席卷您的城市。

加入我们,共赴 Redis 发布盛会

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

本教程展示了如何优化经纪应用程序,演示了使用 Redis 及其 JSON 数据结构和增强查询功能可以实现的功能。

在金融领域,经纪公司的成功与其投资者对其提供的交易应用程序的参与度息息相关。经纪公司致力于创造更好的投资者参与度,因为它会导致管理的资产增加、交易量增加,以及(经纪公司最喜欢的部分,当然)佣金和费用增加。

这些移动应用程序也是展示系统设计一般和 Redis 企业版提供的技术的一种有效方法。经纪公司的应用程序都与时间有关,而作为实时数据平台的 Redis 的精髓,以及与文档存储 (JSON) 和快速查询功能的结合。因此,让我们将此作为为金融服务受众提供服务的移动开发人员的案例研究。

一些经纪应用程序旨在处理任何时候一定数量的客户登录。当交易活动激增时,这些应用程序 面临可扩展性和性能挑战。这些技术故障和糟糕的客户体验对经纪公司的收入、盈利能力和声誉有害。

任何经纪应用程序都有很长的技术必备清单,包括高可用性、低延迟和快速响应时间、强一致性、可扩展性、安全性以及一致的性能。保证所有这些属性是一个挑战。软件开发人员通常使用各种技术和数据库,尽其所能平衡所有这些参数以及互操作性和公司预算的问题。他们的应用程序环境包括 SQL 数据库、NoSQL 数据库和缓存,以创建实时性能和可扩展性的表象。

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

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

如果您愿意,可以 参考示例代码

经纪应用程序的数据模型如何运作

我们使用 JSON 对各种经纪实体进行了建模(图 1)。每个投资者的个人详细信息都建模为名为投资者的 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 文档。账户文档还可以捕获重要数据,例如批准的期权交易水平以及该账户是否允许进行保证金交易。

账户 JSON 文档的键格式如下所示

trading:account:<accountNo>

相应的账户 JSON 文档为

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

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

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

典型的投资者在特定日期和时间以批次购买股票。因此,批次对于账户、证券和购买时间是唯一的。当派发股利时,投资者账户也可能获得新的证券批次。例如,投资者可以在股利再投资计划 (DRIP) 中注册其经纪账户或个人证券。当公司派发股利时,经纪公司为注册 DRIP 的每只股票创建新的证券批次。这只是一个示例。经纪公司可以创建新的证券批次的场景很多,例如公司分拆、兼并和收购以及自动投资计划。

每个此类证券批次的键格式如下所示

trading:securitylot:<accountNo>:<securityLotId>

一个示例证券批次 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 创建索引并定义 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: