Hướng dẫn tích hợp stockprice.Dist

Hệ thống phân phối dữ liệu thị trường chứng khoán thời gian thực qua WebSocket pub/sub.

Tổng quan

stockprice.Dist là hệ thống phân phối dữ liệu thị trường theo thời gian thực dựa trên WebSocket. Hệ thống cung cấp chức năng xuất bản/đăng ký (publish/subscribe) hiệu suất cao theo topic để truyền tải dữ liệu thị trường tài chính bao gồm giá cổ phiếu, dữ liệu chỉ số và dữ liệu biểu đồ lịch sử.

Bắt đầu

Yêu cầu

  • Tài khoản người dùng đang hoạt động với quyền tương ứng theo thỏa thuận dịch vụ
  • Phiên xác thực hợp lệ (sử dụng action Login của AuthPlugin)
  • Client hỗ trợ WebSocket (trình duyệt, Python, v.v.)
  • Truy cập mạng đến endpoint dịch vụ stockprice.Dist

Endpoint WebSocket / HTTP

UAT:

wss://dist-uat.stockprice.vn/v1/ws?Id={sessionId}
https://dist-uat.stockprice.vn/v1/

Production:

wss://dist.stockprice.vn/v1/ws?Id={sessionId}
https://dist.stockprice.vn/v1/

Trong đó {sessionId} là ID phiên được lấy từ header Set-Id trong yêu cầu HTTP đầu tiên, sau đó gửi kèm header Id trong các yêu cầu tiếp theo.

Luồng kết nối

  1. Xác thực qua HTTP: Thực hiện yêu cầu HTTP để lấy session ID từ header Set-Id
  2. Kết nối WebSocket: Kết nối đến endpoint WebSocket với tham số Id
  3. Đăng nhập (nếu dùng endpoint local): Sử dụng action Login của AuthPlugin (alias: l)
  4. Đăng ký các nguồn dữ liệu mong muốn sử dụng action đăng ký
  5. Nhận dữ liệu qua tin nhắn WebSocket dưới dạng đối tượng JSON
  6. Duy trì kết nối — xem mục Keep-Alive bên dưới
  7. Hủy đăng ký khi không còn cần để giải phóng tài nguyên

Keep-Alive

Để duy trì kết nối hoạt động và ngăn chặn timeout phiên, triển khai cả hai cơ chế keep-alive sau (mỗi 60 giây):

HTTP Keep-Alive

Gửi yêu cầu POST đến https://dist.stockprice.vn/v1/P kèm header Id chứa session ID.

WebSocket Keep-Alive

Gửi action Ping qua kết nối WebSocket:

{ "A": "P", "I": request_id }

Ví dụ JavaScript

// HTTP Keep-Alive (mỗi 60 giây)
setInterval(() => {
  fetch('https://dist.stockprice.vn/v1/P', {
    method: 'POST',
    headers: { 'Id': sessionId }
  }).catch(err => console.error('HTTP keep-alive thất bại:', err));
}, 60000);

// WebSocket Keep-Alive (mỗi 60 giây)
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    requestId++;
    ws.send(JSON.stringify({ A: 'P', I: requestId }));
  }
}, 60000);

Ví dụ Python

import threading, time, requests, json

def http_keep_alive(session_id):
    while True:
        try:
            requests.post('https://dist.stockprice.vn/v1/P',
                          headers={'Id': session_id})
        except Exception as e:
            print(f'HTTP keep-alive thất bại: {e}')
        time.sleep(60)

def websocket_keep_alive(ws):
    global request_id
    while True:
        try:
            request_id += 1
            ws.send(json.dumps({'A': 'P', 'I': request_id}))
        except Exception as e:
            print(f'WebSocket keep-alive thất bại: {e}')
        time.sleep(60)

threading.Thread(target=http_keep_alive, args=(session_id,), daemon=True).start()
threading.Thread(target=websocket_keep_alive, args=(ws,), daemon=True).start()

Lưu ý: Nếu một trong hai keep-alive thất bại, thử kết nối lại và khôi phục đăng ký. Thiếu tín hiệu keep-alive có thể dẫn đến timeout phiên.

Định dạng Request/Response

Yêu cầu Action (ActInf)

Tất cả các yêu cầu action sử dụng cấu trúc JSON:

{ "A": "action_alias", "P": "parameters", "I": request_id }
TrườngKiểuBắt buộcMô tả
AstringAlias action (ví dụ: "ss", "sx", "sd")
PstringKhôngTham số (định dạng thay đổi theo action)
InumberCó (WS)ID yêu cầu để khớp phản hồi. BẮT BUỘC cho WebSocket, không cần cho HTTP.

Phản hồi Action (ActRsl)

{ "C": 0, "D": "data_or_message", "I": request_id }
TrườngKiểuMô tả
CintMã kết quả (0 = thành công, 1+ = lỗi)
DstringDữ liệu hoặc thông báo lỗi
InumberID yêu cầu trả lại từ yêu cầu gốc

Tin nhắn dữ liệu (Data Message)

Dữ liệu được đẩy từ máy chủ dưới dạng:

{ "t": "topic_name", "d": "csv_data" }

Khi snapshot ban đầu hoàn thành, máy chủ gửi tín hiệu hoàn thành (chỉ có trường t, không có d):

{ "t": "topic_name" }

Mẫu bộ đếm yêu cầu

let requestId = 0;

function sendRequest(action, params) {
  requestId++;
  ws.send(JSON.stringify({ A: action, P: params, I: requestId }));
  return requestId;
}

Bảo mật

Bảng tham khảo nhanh

ActionAliasTham sốMô tả
GetServerTime0KhôngLấy giờ trên máy chủ
GetRSAPublicKeyrKhôngLấy public key RSA
LoginlUsername/mật khẩu đã mã hóa RSAĐăng nhập vào máy chủ
LogoutoKhôngĐăng xuất khỏi phiên
ChangePasswordcMật khẩu cũ/mới được mã hóa RSAĐổi mật khẩu

GetServerTime

// Yêu cầu
{ "A": "0" }

// Phản hồi
{ "C": 0, "D": "1690207350" }

GetRSAPublicKey

// Yêu cầu
{ "A": "r" }

// Phản hồi
{ "C": 0, "D": "{\"Exponent\":\"AQAB\",\"Modulus\":\"xqBYwy...vQ==\"}" }

Login

Tham số đầu vào là chuỗi được mã hóa RSA với cấu trúc: random;username;password

// Yêu cầu
{ "A": "l", "P": "...chuỗi đã mã hóa RSA..." }

// Phản hồi thành công (D = thời gian hết hạn epoch)
{ "C": 0, "D": "1680702231" }

// Phản hồi lỗi
{ "C": 1, "D": "Invalid credentials" }

random là chuỗi ngẫu nhiên do client sinh ra. usernamepassword là tài khoản đăng nhập.

Logout

// Yêu cầu
{ "A": "o" }

// Phản hồi
{ "C": 0, "D": null }

ChangePassword

Tham số đầu vào: chuỗi mã hóa RSA với cấu trúc random;old_password;new_password

// Yêu cầu
{ "A": "c", "P": "...chuỗi đã mã hóa RSA...", "S": null }

// Phản hồi
{ "C": 0, "D": null }

Đăng ký dữ liệu thời gian thực

SubscribeStockData (ss)

Đăng ký cập nhật giá cổ phiếu thời gian thực cho tất cả các cổ phiếu. Topic: stock_data

// Yêu cầu
{ "A": "ss", "I": 1 }

// Phản hồi
{ "C": 0, "D": "success", "I": 1 }

// Tin nhắn dữ liệu
{ "t": "stock_data", "d": "SSI,75.5,78.3,72.7,74.0,100,..." }

// Tín hiệu hoàn thành
{ "t": "stock_data" }

SubscribeIndexData (sx)

Đăng ký cập nhật dữ liệu chỉ số thời gian thực cho tất cả các chỉ số. Topic: index_data

// Yêu cầu
{ "A": "sx", "I": 2 }

// Phản hồi
{ "C": 0, "D": "success", "I": 2 }

// Tin nhắn dữ liệu
{ "t": "index_data", "d": "VNINDEX,1640995200,O,1200.5,1205.5,..." }

GetStockInfo — WebSocket (ssi)

Yêu cầu thông tin cổ phiếu (đẩy một lần, không phải đăng ký liên tục). Topic: stock_info

// Yêu cầu
{ "A": "ssi", "I": 3 }

// Phản hồi
{ "C": 0, "D": "success", "I": 3 }

// Tin nhắn dữ liệu
{ "t": "stock_info", "d": "SSI,HSX,STK,STX,SSI Securities Inc.,..." }

GetStockInfo — HTTP (ssi)

Lấy thông tin cổ phiếu cho các mã cụ thể qua HTTP (tối đa 50 mã).

POST https://dist.stockprice.vn/v1/S HTTP/1.1
Content-Type: application/json
Id: your-session-id

{ "A": "ssi", "P": "SSI,VNM,HDB" }
// Phản hồi thành công
{ "C": 0, "D": "SSI,HSX,STK,STX,...\nVNM,HSX,STK,STX,..." }

// Phản hồi lỗi
{ "C": 1, "D": "Maximum 50 symbols allowed" }

Đăng ký dữ liệu lịch sử

Định dạng tham số chung: mã1,lastKey1\nmã2,lastKey2\n...

Trong đó lastKey là Unix epoch timestamp (giây). Dùng 0 để lấy tất cả dữ liệu có sẵn.

SubscribeHistD (sd) — Dữ liệu theo ngày

Topics: D/{MÃ} (ví dụ: D/SSI, D/VNM)

// Yêu cầu
{ "A": "sd", "P": "SSI,1640995200\nVNM,0\nHDB,1641081600", "I": 4 }

// Phản hồi
{ "C": 0, "D": "success", "I": 4 }

// Tin nhắn dữ liệu (cho mỗi mã)
{ "t": "D/SSI", "d": "1640995200,1.0,75.5,76.0,77.0,74.5,5000000,..." }

// Tín hiệu hoàn thành (cho mỗi mã)
{ "t": "D/SSI" }

SubscribeHistM (sm) — Dữ liệu theo phút

Topics: M/{MÃ} (ví dụ: M/SSI, M/VNM)

// Yêu cầu
{ "A": "sm", "P": "SSI,1640995200\nVNM,0", "I": 5 }

// Phản hồi
{ "C": 0, "D": "success", "I": 5 }

// Tin nhắn dữ liệu
{ "t": "M/SSI", "d": "1640995200,75.5,76.0,75.0,75.8,10000,..." }

SubscribeTrade (st) — Dữ liệu giao dịch

Topics: T/{MÃ} (ví dụ: T/SSI, T/VNM). Lưu ý: Dữ liệu giao dịch KHÔNG có sẵn cho các mã chỉ số.

// Yêu cầu
{ "A": "st", "P": "SSI,1640995200\nVNM,0", "I": 6 }

// Phản hồi thành công
{ "C": 0, "D": "success", "I": 6 }

// Phản hồi lỗi (nếu bao gồm mã chỉ số)
{ "C": 1, "D": "Trade data not available for indices", "I": 6 }

// Tin nhắn dữ liệu
{ "t": "T/SSI", "d": "1640995200,75.5,1000,B\r\n1640995205,75.6,500,S" }

Hủy đăng ký

Tất cả các action hủy đăng ký không yêu cầu kiểm tra quyền.

ActionAliasTham sốMô tả
UnsubscribeStockDatausKhôngHủy đăng ký giá cổ phiếu
UnsubscribeIndexDatauxKhôngHủy đăng ký dữ liệu chỉ số
UnsubscribeHistDudDanh sách mã hoặc rỗngHủy đăng ký dữ liệu theo ngày
UnsubscribeHistMumDanh sách mã hoặc rỗngHủy đăng ký dữ liệu theo phút
UnsubscribeTradeutDanh sách mã hoặc rỗngHủy đăng ký dữ liệu giao dịch
UnsubscribeAlluaKhôngHủy đăng ký tất cả các topic

Ví dụ

// Hủy đăng ký giá cổ phiếu
{ "A": "us", "I": 7 }

// Hủy đăng ký các mã lịch sử cụ thể
{ "A": "ud", "P": "SSI,VNM,HDB", "I": 9 }

// Hủy đăng ký tất cả dữ liệu theo phút
{ "A": "um", "P": "", "I": 12 }

// Hủy đăng ký tất cả
{ "A": "ua", "I": 15 }

Truy xuất dữ liệu qua HTTP

Các action sau cung cấp truy cập dữ liệu thị trường qua HTTP mà không yêu cầu kết nối WebSocket. Hữu ích cho polling đơn giản hoặc các nền tảng không hỗ trợ WebSocket.

  • Yêu cầu HTTP KHÔNG yêu cầu trường I
  • Yêu cầu WebSocket YÊU CẦU trường I
  • Tất cả gửi qua POST đến https://dist.stockprice.vn/v1/S kèm header Id

GetStockData (gs)

Lấy dữ liệu cổ phiếu hiện tại. Tham số: lastChangeId (tùy chọn, mặc định 0).

// Yêu cầu HTTP
{ "A": "gs", "P": "12345" }

// Phản hồi
{ "C": 0, "D": "{\"csv\":\"SSI,75.5,...\\nVNM,82.3,...\",\"dataId\":12450}" }

Sử dụng dataId trong yêu cầu tiếp theo để chỉ lấy thay đổi mới (cập nhật gia tăng).

GetIndexData (gi)

Lấy dữ liệu chỉ số hiện tại. Tham số: lastChangeId (tùy chọn, mặc định 0).

// Yêu cầu HTTP
{ "A": "gi", "P": "0" }

// Phản hồi
{ "C": 0, "D": "{\"csv\":\"VNINDEX,1640995200,O,1200.5,...\",\"dataId\":8920}" }

GetHistD (gd) — Lịch sử theo ngày

Tham số: JSON {"symbol":"...", "lastTime":...}

// Yêu cầu HTTP
{ "A": "gd", "P": "{\"symbol\":\"SSI\",\"lastTime\":1640995200}" }

// Phản hồi (CSV trực tiếp)
{ "C": 0, "D": "1640995200,1.0,75.5,76.0,77.0,74.5,5000000,...\n..." }

GetHistM (gm) — Lịch sử theo phút

// Yêu cầu HTTP
{ "A": "gm", "P": "{\"symbol\":\"SSI\",\"lastTime\":1640995200}" }

// Phản hồi (CSV trực tiếp)
{ "C": 0, "D": "1640995200,75.5,76.0,75.0,75.8,10000,...\n..." }

GetHistTrade (gt) — Lịch sử giao dịch

Lưu ý: Dữ liệu giao dịch KHÔNG có sẵn cho các mã chỉ số.

// Yêu cầu HTTP
{ "A": "gt", "P": "{\"symbol\":\"SSI\",\"lastTime\":1640995200}" }

// Phản hồi
{ "C": 0, "D": "1640995200,75.5,1000,B\n1640995205,75.6,500,S\n..." }

Ví dụ HTTP Polling (JavaScript)

let lastDataId = 0;

async function pollStockData() {
  const response = await fetch('https://dist.stockprice.vn/v1/S', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Id': sessionId },
    body: JSON.stringify({ A: 'gs', P: lastDataId.toString() })
  });
  const result = await response.json();
  if (result.C === 0) {
    const data = JSON.parse(result.D);
    console.log('Stock CSV:', data.csv);
    lastDataId = data.dataId; // Lưu cho yêu cầu tiếp theo
  }
}

setInterval(pollStockData, 1000);

Đặc tả định dạng dữ liệu

Tổng quan định dạng CSV

  • Các trường cách nhau bởi dấu phẩy ,
  • Nhiều bản ghi cách nhau bởi \n (xuống dòng)
  • Trường rỗng = giá trị mặc định/zero
  • Kiểu số thực: 75.5, rỗng = 0.0
  • Kiểu số nguyên: 1000, rỗng = 0
  • Timestamp: Unix epoch (giây, giờ địa phương), rỗng = null

StockData (topic: stock_data)

Dữ liệu giá cổ phiếu thời gian thực với độ sâu bid/ask tối đa 10 cấp (68 trường).

#TrườngKiểuMô tả
1SymbolstringMã cổ phiếu
2RefPxdoubleGiá tham chiếu
3CeilingPxdoubleGiá trần
4FloorPxdoubleGiá sàn
5-6BestBidPx3, BestBidVol3double, longGiá/KL mua cấp 3
7-8BestBidPx2, BestBidVol2double, longGiá/KL mua cấp 2
9-10BestBidPx1, BestBidVol1double, longGiá/KL mua cấp 1 (cao nhất)
11-12LastMatchedPx, LastMatchedVoldouble, longGiá/KL khớp lệnh gần nhất
13-14BestAskPx1, BestAskVol1double, longGiá/KL bán cấp 1 (thấp nhất)
15-18BestAskPx2-3, BestAskVol2-3double, longGiá/KL bán cấp 2-3
19-20TotalVolTraded, TotalValTradedlong, longTổng KL/Giá trị giao dịch
21-23OpenPx, HighPx, LowPxdoubleGiá mở/cao/thấp trong ngày
24-27SellVol, SellVal, BuyVol, BuyVallongTổng KL/Giá trị mua/bán
28-31PtMatchedPx, PtMatchedVol, PtVol, PtValdouble, longThỏa thuận
32-34CurrentRoom, OpenInterest, Inavlong, long, doubleRoom NN, Vị thế mở, iNAV
35-38FrnBuyVol, FrnBuyVal, FrnSellVol, FrnSellVallongGiao dịch nước ngoài
39-40TotalBidVol, TotalAskVollongTổng KL đặt mua/bán
41-68BestBidPx4-10, BestAskPx4-10...double, longCấp giá mua/bán 4-10 (độ sâu mở rộng)

IndexData (topic: index_data)

Dữ liệu chỉ số thị trường thời gian thực (27 trường).

#TrườngKiểuMô tả
1SymbolstringMã chỉ số (VNINDEX, VN30...)
2TradingTimelongNgày giao dịch (epoch)
3StatusCodestringTrạng thái: "O"=Mở, "C"=Đóng
4RefIndexdoubleChỉ số tham chiếu
5MarketIndexdoubleChỉ số hiện tại
6-8HighIndex, LowIndex, OpenIndexdoubleCao/Thấp/Mở cửa
9-10Vol, VallongTổng KL/Giá trị
11-16Advances, Ceiling, NoChanges, NoTrade, Declines, FloorintThống kê mã tăng/giảm/trần/sàn
17-26PtVol...FrnSellVallongThỏa thuận, mua/bán, nước ngoài
27TotalCountintTổng số mã trong chỉ số

StockInfo (topic: stock_info)

Thông tin cổ phiếu tĩnh (23 trường).

#TrườngKiểuMô tả
1SymbolstringMã cổ phiếu
2ExchangeIdstringSàn (HSX, HNX, UPCOM)
3-4MarketId, GroupIdstringMã thị trường, nhóm ngành
5-6Name, NameEnstringTên công ty (VI/EN)
7-10Address, Phone, Fax, WebsitestringThông tin liên hệ
11-13ListedVol, ListedDate, TradingVollongKL niêm yết, ngày, KL giao dịch
14-21MaturityDate...ExerciseRatiomixedThông tin chứng quyền
22-23OpenInterest, TotalRoomlongVị thế mở, Room NN

DData (topic: D/{MÃ}) — Dữ liệu theo ngày

Dữ liệu nến theo ngày (24 trường).

#TrườngKiểuMô tả
1TradingTimelongNgày giao dịch (epoch)
2AdjustRatedoubleTỷ lệ điều chỉnh (1.0 = không điều chỉnh)
3RefPxdoubleGiá tham chiếu
4-7OpenPx, ClosePx, HighPx, LowPxdoubleOHLC
8-9Vol, VallongTổng KL/Giá trị
10-17BuyVol...FrnSellVallongMua/bán, nước ngoài
18-19PtVol, PtVallongThỏa thuận
20OpenInterestlongVị thế mở (phái sinh)
21-24PortBuyVol...PortSellVallongDanh mục đầu tư

Lưu ý AdjustRate: Khi phân tích kỹ thuật, nhân giá với AdjustRate / 1000000. Khi AdjustRate thay đổi, cần tính lại cho tất cả các ngày trước đó.

MData (topic: M/{MÃ}) — Dữ liệu theo phút

Dữ liệu nến theo phút (14 trường).

#TrườngKiểuMô tả
1TradingTimelongThời gian (epoch)
2-5OpenPx, HighPx, LowPx, ClosePxdoubleOHLC
6VollongTổng KL
7-10BuyVol, BuyVal, SellVol, SellVallongMua/bán
11-14FrnBuyVol, FrnBuyVal, FrnSellVol, FrnSellVallongNước ngoài

TradeData (topic: T/{MÃ}) — Dữ liệu giao dịch

Dữ liệu giao dịch theo từng lệnh (4 trường).

#TrườngKiểuMô tả
1TradingTimelongTimestamp giao dịch (epoch)
2ClosePxdoubleGiá giao dịch
3VollongKhối lượng giao dịch
4Sidestring"B" (Mua), "S" (Bán), rỗng (không xác định)

Tên Topic

Topics thời gian thực

  • stock_data — Giá cổ phiếu thời gian thực cho tất cả cổ phiếu
  • index_data — Dữ liệu chỉ số thời gian thực cho tất cả chỉ số
  • stock_info — Thông tin cổ phiếu (đẩy một lần)

Topics lịch sử

  • D/{MÃ} — Dữ liệu lịch sử theo ngày (ví dụ: D/SSI, D/VNM, D/VNINDEX)
  • M/{MÃ} — Dữ liệu lịch sử theo phút (ví dụ: M/SSI, M/VNM)
  • T/{MÃ} — Dữ liệu giao dịch (chỉ cổ phiếu, ví dụ: T/SSI, T/VNM)

Quy tắc đặt tên

  • Topics thời gian thực sử dụng tên viết thường đơn giản
  • Topics lịch sử sử dụng định dạng: {LoạiDữLiệu}/{Mã}
  • Mã không phân biệt chữ hoa/thường (tự động chuyển sang IN HOA)
  • Mã chỉ số CÓ THỂ dùng với D và M
  • Mã chỉ số KHÔNG THỂ dùng với T

Xử lý lỗi

Lỗi thường gặp

LỗiNguyên nhânGiải pháp
"WebSocket was not connected"Kết nối WS chưa thiết lập hoặc đã đóngĐảm bảo kết nối WS đang mở trước khi gửi action
"Missing symbol list"Tham số P bị thiếu hoặc rỗngCung cấp danh sách mã đúng định dạng
"Trade data not available for indices"Đăng ký T cho mã chỉ sốLoại bỏ mã chỉ số, dùng D hoặc M thay thế
"Maximum 50 symbols allowed"Vượt quá giới hạn HTTP ssiGiảm số mã xuống dưới 50

Thực hành tốt

  1. Luôn kiểm tra mã phản hồi (trường C): 0 = thành công, 1+ = lỗi
  2. Triển khai logic kết nối lại với backoff theo hàm mũ
  3. Khôi phục đăng ký sau khi kết nối lại
  4. Xác thực tham số trước khi gửi (định dạng CSV, mã hợp lệ)
  5. Mã không hợp lệ được bỏ qua âm thầm (không lỗi, chỉ không có dữ liệu)
  6. Sử dụng action Login của AuthPlugin trước khi đăng ký

Ví dụ Code

JavaScript — Kết nối và đăng ký cơ bản

const ws = new WebSocket('wss://dist.stockprice.vn/v1/ws?Id=' + sessionId);
let requestId = 0;

ws.onopen = function() {
  // Đăng ký dữ liệu cổ phiếu
  requestId++;
  ws.send(JSON.stringify({ A: 'ss', I: requestId }));

  // Đăng ký dữ liệu lịch sử theo ngày
  requestId++;
  ws.send(JSON.stringify({ A: 'sd', P: 'SSI,0\nVNM,0', I: requestId }));
};

ws.onmessage = function(event) {
  const msg = JSON.parse(event.data);

  // Phản hồi action
  if ('C' in msg) {
    if (msg.C === 0) console.log('OK:', msg.I, msg.D);
    else console.error('Lỗi:', msg.I, msg.D);
    return;
  }

  // Tin nhắn dữ liệu
  if ('t' in msg) {
    if ('d' in msg) {
      const lines = msg.d.split('\n');
      lines.forEach(line => {
        if (!line) return;
        const fields = line.split(',');
        console.log(msg.t, fields[0], fields);
      });
    } else {
      console.log('Hoàn thành:', msg.t);
    }
  }
};

JavaScript — Đăng ký nhiều loại dữ liệu

function subscribeAll(ws) {
  // Giá cổ phiếu thời gian thực
  ws.send(JSON.stringify({ A: 'ss', I: ++requestId }));
  // Chỉ số thời gian thực
  ws.send(JSON.stringify({ A: 'sx', I: ++requestId }));
  // Thông tin cổ phiếu
  ws.send(JSON.stringify({ A: 'ssi', I: ++requestId }));
  // Lịch sử theo ngày
  ws.send(JSON.stringify({ A: 'sd', P: 'SSI,1640995200\nVNM,0', I: ++requestId }));
  // Lịch sử theo phút
  ws.send(JSON.stringify({ A: 'sm', P: 'SSI,1640995200', I: ++requestId }));
  // Giao dịch (chỉ cổ phiếu)
  ws.send(JSON.stringify({ A: 'st', P: 'SSI,0\nVNM,0', I: ++requestId }));
}

Python — Kết nối WebSocket cơ bản

import websocket
import json
import threading

request_id = 0

def get_next_id():
    global request_id
    request_id += 1
    return request_id

def on_message(ws, message):
    msg = json.loads(message)
    if 'C' in msg:
        print(f"{'OK' if msg['C']==0 else 'Lỗi'} ({msg.get('I')}): {msg.get('D')}")
    elif 't' in msg:
        if 'd' in msg:
            lines = msg['d'].split('\n')
            for line in lines:
                if line:
                    fields = line.split(',')
                    print(f"  {msg['t']}: {fields[:5]}...")
        else:
            print(f"  Hoàn thành: {msg['t']}")

def on_open(ws):
    # Đăng ký dữ liệu cổ phiếu
    ws.send(json.dumps({'A': 'ss', 'I': get_next_id()}))
    # Đăng ký lịch sử
    ws.send(json.dumps({'A': 'sd', 'P': 'SSI,0\nVNM,0', 'I': get_next_id()}))

ws = websocket.WebSocketApp(
    f'wss://dist.stockprice.vn/v1/ws?Id={session_id}',
    on_message=on_message,
    on_open=on_open,
    on_error=lambda ws, e: print(f'Lỗi: {e}'),
    on_close=lambda ws, c, m: print('Đã đóng')
)
ws.run_forever()

Python — Lấy dữ liệu lịch sử qua HTTP

import requests, json

def get_historical(session_id, symbol, action, last_time=0):
    """action: 'gd' (ngày), 'gm' (phút), 'gt' (giao dịch)"""
    params = {"symbol": symbol, "lastTime": last_time}
    resp = requests.post(
        'https://dist.stockprice.vn/v1/S',
        headers={'Content-Type': 'application/json', 'Id': session_id},
        json={'A': action, 'P': json.dumps(params)}
    )
    result = resp.json()
    if result['C'] == 0:
        for line in result['D'].split('\n'):
            if line:
                print(line.split(','))
    else:
        print(f"Lỗi: {result['D']}")

# Sử dụng
get_historical(session_id, 'SSI', 'gd', 0)   # Theo ngày
get_historical(session_id, 'VNM', 'gm', 0)   # Theo phút
get_historical(session_id, 'HDB', 'gt', 0)   # Giao dịch