Danh mục đầu tư (Portfolio) là nền tảng của ngành quản lý tài sản và tài sản. Từ khi Harry Markowitz khởi xướng lý thuyết portfolio hiện đại, các chuyên gia quản lý tài sản luôn khao khát tối đa hóa lợi nhuận cho mức rủi ro cho trước. Ngày nay, hàng triệu nhà đầu tư lẻ gia nhập, thay đổi vĩnh viễn bối cảnh đầu tư. Những người mới này tạo ra tác động lớn đến công nghệ hỗ trợ hạ tầng giao dịch của các nhà môi giới, sàn giao dịch và clearing house.
Ví dụ, cú hích cổ phiếu GameStop tháng 1/2021. Nhà đầu tư lẻ giao dịch cổ phiếu GameStop ở mức kỷ lục. Họ cũng đổ tiền vào các "meme stock" khác như AMC Entertainment, khiến biến động thị trường tăng hơn 76% chỉ trong vài phiên giao dịch theo VIX. Biến động này gây áp lực giá lên hàng nghìn chứng khoán. Hàng triệu nhà đầu tư cố gắng truy cập portfolio cùng lúc, nhưng đối mặt với app không đáp ứng được nhu cầu. Nhà đầu tư không bao giờ nhân ái với công ty app kém hiệu suất khi họ cần nhất.
Trong những lúc hoảng loạn này, hầu hết nhà đầu tư tìm 2 điểm dữ liệu về portfolio cần truy cập mọi lúc:
- Tổng giá trị portfolio tại thời điểm đó là bao nhiêu?
- Lãi/lỗ của từng chứng khoán cụ thể trong portfolio?
Câu trả lời dẫn dắt nhà đầu tư quyết định mua, bán, hoặc giữ. Trong thị trường nhanh ngày nay, bất kỳ độ trễ nào cũng có thể nghĩa là mất cơ hội và lợi nhuận. Bạn cần truy cập real-time vào giá — nhưng có 2 thách thức lớn:
- Cập nhật giá hàng nghìn chứng khoán đồng thời
- Phục vụ hàng triệu request khách hàng cùng lúc
Giá chứng khoán thay đổi nhanh dựa trên volume giao dịch, biến động của từng chứng khoán và biến động thị trường. Mặt khác, một nhà môi giới có thể có hàng triệu khách hàng, mỗi người có chục chứng khoán trong portfolio. Ngay khi khách hàng đăng nhập, portfolio cần cập nhật giá mới nhất — và giữ cập nhật khi nhà môi giới nhận giá từ sàn giao dịch.
Về cơ bản, chúng ta đang tạo biểu đồ cổ phiếu real-time. Nhiều app môi giới không cố gắng làm điều này ở quy mô lớn. Thay vào đó, các app pull giá mới nhất thay vì push giá đến hàng triệu client. Ví dụ, có nút refresh trên trang portfolio.
Những thách thức thế hệ mới này không hề tầm thường và không dễ giải quyết bằng database dựa trên disk, vốn không thiết kế cho hàng triệu operation/giây. Ngành tài chính cần database scale dễ dàng và xử lý hàng trăm triệu operation/giây. Redis Enterprise, platform database in-memory, có tiềm năng giải quyết những thách thức này.
Đây là bài đầu tiên trong series blog về các use case real-time trong tài chính. Chúng tôi sẽ cover chi tiết, thách thức kinh doanh và vai trò của Redis Enterprise. Cũng cung cấp sample design, data model, code sample, và thảo luận ưu/nhược điểm.
Trong bài này chúng ta cover:
- Sample implementation mô hình dữ liệu portfolio chứng khoán hiệu năng cao, scalable trên Redis Enterprise.
- Cập nhật giá chứng khoán trong portfolio real-time khi nhận giá mới từ sàn giao dịch.
Sau khi client app lấy portfolio và nhận giá mới, nó có thể:
- Tính tổng giá trị portfolio.
- Tính lãi/lỗ từng holding.
Mô hình dữ liệu Portfolio Chứng Khoán
Bắt đầu bằng mô hình một holding trong portfolio. Trong minh họa dưới, CVS Health Corp. (NYSE: CVS) là holding ví dụ. Có 2 lot CVS riêng biệt — lot đầu mua ngày 4/1/2021, lot thứ 2 ngày 1/3/2021. Cùng số lượng cổ phần mua trong trade buy cho mỗi lot. Cả 2 trade đều 10 cổ phần, nhưng giá mỗi cổ phần khác — $68.3378 cho lot 1 và $68.82 cho lot 2. Tổng số lượng holding CVS là 20 với chi phí trung bình: (($68.3378 * 10) + ($68.82 * 10))/20 = $68.5789/cổ phần.
Triển khai yêu cầu
Biểu diễn dữ liệu của Redis là phẳng — không thể nhúng Set trong Set khác. Do đó mô hình dữ liệu theo ER diagram không thể implement trực tiếp. Implement trực tiếp Entity Model có thể không cho performance mong muốn, nên bạn phải suy nghĩ khác khi implement. Phần này cover một số nguyên tắc thiết kế cơ bản khi thiết kế implementation hiệu năng cao, scalable dùng Redis.
Mô hình dữ liệu nhắc đến các entity sau:
ER diagram cung cấp biểu diễn trực quan giúp thấy rõ vấn đề.
Điều thiếu trong diagram là tập giá đến (incoming prices) dù chúng được ghi trong lịch sử giá của chứng khoán — và tính toán giá trị tức thời và lãi/lỗ khi giá thay đổi. Vì vậy ER diagram đại diện cho dữ liệu tĩnh tương đối là bối cảnh portfolio valuation diễn ra.
Kiến trúc tổng thể
Các điểm quan trọng cần xem xét:
- Timeliness (latency) là yếu tố then chốt. Đây là thuộc tính điều khiển tổng thể.
- Tính toán portfolio value kết hợp data riêng tài khoản (thông tin lot) với data chia sẻ giữa tài khoản (giá chứng khoán). Data riêng tài khoản là bối cảnh dùng data chia sẻ.
- Portfolio value chỉ cần tính cho tài khoản mà investor đang online (một phần nhỏ tổng tài khoản).
- Data chảy qua hệ thống, từ sàn giao dịch sản xuất giá ra máy client (browser, mobile app) hiển thị portfolio value cho investor online.
- Khu vực động cao: tỷ lệ luồng data đầu vào và số investor online.
Dựa trên những điểm này, các hướng tiếp cận chung:
- Dựa vào kiến trúc in-memory của Redis cho truy cập low-latency vào data tĩnh và động.
- Tối ưu data modeling dùng cấu trúc dữ liệu Redis để truy cập nhanh data bối cảnh thay đổi chậm.
- Dùng cấu trúc communication của Redis (Streams, Consumer Groups, Pub/Sub) xử lý yêu cầu data động.
- Giảm thiểu lưu trữ data chỉ cần thiết mà không ảnh hưởng performance tổng thể.
- Implement tính toán client-specific trên chính client. Điều này scale tự nhiên với số investor online, giảm gánh nặng scale đáng kể.
Các thành phần tính toán chính và luồng data:
Lưu ý Redis Enterprise gồm 1 hoặc nhiều node trên nhiều máy — triển khai on-premises, Kubernetes, hybrid cloud, managed service, hoặc native first-party cloud service — và sẽ có hàng trăm nghìn investor online với client tùy chọn.
Thành phần Redis Enterprise
Cập nhật giá chứng khoán sẽ được hấp thụ bởi Redis Streams. Cập nhật cho các chứng khoán sẽ trộn lẫn trong stream này và cần tách biệt (disaggregate) để data hữu ích. Consumer group sẽ dùng để disaggregation đó, và xử lý data thành 2 cấu trúc, per security:
- Database RedisTimeSeries, để theo dõi lịch sử thay đổi giá (cũng ghi giá mới nhất cho client vừa kết nối)
- Pub/Sub broker channel để push thông báo thay đổi giá đến client đã subscribe channel đó (investor portfolio chứa chứng khoán đó)
Sơ đồ dưới chi tiết phần kiến trúc này:
Yếu tố quan trọng nhất trong mô hình là data riêng tài khoản đại diện cho lot và chứng khoán liên quan. Chúng ta so sánh 2 implementation làm ví dụ về cách suy nghĩ modeling data trong Redis, tập trung vào performance. Implementation khác cũng có thể — mục tiêu là giới thiệu nguyên tắc thiết kế và quy trình tư duy khi implement data trong Redis.
Chúng ta dùng thông tin sau làm ví dụ cụ thể:
Chúng ta định giá theo đơn vị tiền tệ nhỏ nhất để tránh float và giữ mọi thứ ở integer. Cho phép client xử lý chuyển đổi sang dollar và cent. Ví dụ này dùng giá độ chính xác 2 chữ số thập phân.
Data Model A
Implementation đầu ghi id của tất cả lot trong tài khoản dùng SET, identified by account id, rồi dùng 1 Redis HASH per LOT, identified by LOT ID, với field ticker, quantity, purchase price. Nói cách khác, dùng HASH mô hình cấu trúc entity LOT, mỗi thuộc tính của LOT entity là field trong Redis HASH.
Với data model này, ta có key cho mỗi account và value chứa tất cả LOT ID cho account đó lưu as Redis SET:
lotids:<ACCOUNT_ID> SET <LOTID>
Thêm vào đó, cho mỗi lotid, ta có HASH với field là ticker, quantity, purchase price:
lot:<LOTID> HASH <ticker TICKER> <quantity INTEGER> <price INTEGER>
Cụ thể, tạo key như sau:
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
Module RedisTimeSeries cho phép lưu và truy xuất cặp time-value liên quan, cùng high-volume inserts và low-latency reads. Chúng ta lấy giá mới nhất cho ticker client quan tâm dùng time series key tương ứng:
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
và subscribe pricing channel để nhận update:
<TICKER> SUBSCRIPTION_CHANNEL
Để lấy tất cả data, client thực hiện:
- 1 lần SMEMBERS trên key lotids — time complexity O(N) với N là số lot
- N lần HGETALL trên key lot — time complexity N lần O(1)
- T lần TS.GET trên key price_history — time complexity T lần O(1) với T là số ticker
- 1 lần SUBSCRIBE trên channel <TICKER> — time complexity O(T) (có thể subscribe tất cả channel trong 1 lần gọi SUBSCRIBE)
Time complexity tổng thể O(N + T).
Cụ thể, operation 1 và 2:
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"
Có thể minimize network latency bằng pipelining (batching client-side) và/hoặc dùng LUA scripts lặp lại (SCRIPT LOAD & EVALSHA). Ghi chú: Transaction implement bằng pipeline có thể giảm network latency, nhưng client-specific và mục tiêu atomicity trên server, nên không giải quyết network latency. Pipeline gồm commands có input/output độc lập. LUA scripts yêu cầu tất cả keys cung cấp trước và tất cả keys hash vào cùng slot (xem Redis Enterprise docs).
Với ràng buộc này, assignment operations vào pipeline:
- pipeline 1: Single command operation #1
- pipeline 2: Tất cả N commands operation #2
- pipeline 3: Tất cả N commands operations #3 và #4
Và dùng LUA scripts không thể vì mỗi operation dùng key khác nhau và key không có phần chung hash vào cùng slot.
Sử dụng model này, time complexity O(N+T) và 3 network hops.
Data Model B
Model thay thế: flatten cấu trúc entity LOT và biểu diễn mỗi thuộc tính entity bằng key identified by account id — một key cho mỗi thuộc tính (quantity, ticker, price) của lot. Field trong mỗi HASH sẽ là LOT ID và value tương ứng quantity, ticker, hoặc price. Do đó key:
tickers_by_lot: <ACCOUNT_ID> HASH <LOTID TICKER>
quantities_by_lot:<ACCOUNT_ID> HASH <LOTID INTEGER>
prices_by_lot:<ACCOUNT_ID> HASH <LOTID INTEGER>
Các hash này thay thế key LOTID và LOT từ Data Model A, trong khi price_history và <TICKER> key giữ nguyên.
Tạo key:
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
Truy xuất giá trị:
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"
Operations client yêu cầu bây giờ:
- 1 lần HGETALL trên key lot_quantity — time complexity N x O(1)
- 1 lần HGETALL trên key lot_ticker — time complexity N x O(1)
- 1 lần HGETALL trên key lot_price — time complexity N x O(1)
- T lần TS.GET trên key price_history — time complexity T x O(1) với T là số ticker
- 1 lần SUBSCRIBE trên channel <TICKER> — time complexity 1 x O(T)
Time complexity tổng thể O(N+T) — giống như trước.
Về pipeline perspective:
- Pipeline one — tất cả commands operations #1, #2, #3
- Pipeline two — tất cả T commands operations #4 và #5
Vậy chúng ta giảm 1 network hop — không nhiều tuyệt đối, nhưng 33% về mặt tương đối.
Thêm vào đó, dễ dàng dùng LUA vì biết keys, và có thể map tất cả keys cho account cụ thể vào cùng slot. Với sự đơn giản của operations, không đi sâu LUA thêm, nhưng lưu ý design này làm cho nó ít nhất là có thể!
Trong benchmark đơn giản, Data Model B nhanh hơn 4.13 ms (benchmarked trên hàng nghìn run). Vì chỉ chạy 1 lần mỗi khi client initialized cho account, có thể không ảnh hưởng performance tổng thể.
Tóm tắt
Trong blog này, chúng ta đã show 2 implementation có thể của Entity model dùng Redis data types. Cũng giới thiệu phân tích time complexity nên làm khi chọn Redis data type, cùng cân nhắc cải thiện network performance — bước critical khi cần large scale và high performance. Trong các blog sau, chúng ta sẽ mở rộng thêm khi data model mở rộng.
Chúng ta đã giới thiệu một số thách thức kinh doanh quản lý portfolio chứng khoán ở quy mô lớn và show:
- Redis data model implement portfolio chứng khoán real-time, scalable.
- Hệ thống cập nhật giá real-time hiệu năng cao dùng để tính tổng giá trị portfolio và lãi/lỗ từng holding.
Với 2 tính năng critical này, brokerage app client cung cấp real-time portfolio updates perform và scale xử lý hàng triệu account. Design này present tổng giá trị portfolio và gain/loss từng holding real-time. Data model và architecture này cũng áp dụng cho use case ngoài chứng khoán: crypto, ad exchanges, v.v.