Thứ Tư, 15 tháng 4, 2026
Đăng bởi

Sau v1.2 (Symbol Directory + Market Calendar), mình nhận được feedback từ hai phía người dùng rất khác nhau:
v1.3 trả lời cả hai.
vnstock quote VCBCài global một lần:
npm install -g vnstock-js
Sau đó gõ bất kỳ đâu:
$ vnstock quote VCB
VCB HOSE
Giá 85.50 (+0.50 +0.59%)
Trần 91.50
Sàn 79.50
TC 85.00
KL 1,234,567
Không cần mở editor, không cần viết script. Trong 1 tuần dùng, mình nhận ra CLI thay đổi cách mình làm việc với data thị trường hoàn toàn.
vnstock quote <SYMBOL> — snapshot 1 mãvnstock history <SYMBOL> --range 30d — OHLCVvnstock search "ngân hàng" — tìm mã theo tênvnstock symbols --exchange HOSE — liệt kê mã theo sànMọi command hỗ trợ --json và --csv để pipe ra file hoặc các tool khác:
# Export 1 năm giá ra CSV
vnstock history VCB --from 1y --csv > vcb-1y.csv
# Pipe qua jq
vnstock quote FPT --json | jq '.price'
Xem hướng dẫn CLI đầy đủ.
npxnpx vnstock-js quote VCB
Dùng xong, máy sạch, không lem vào PATH.
Trước v1.3, nếu bạn muốn giá nhảy live trên web, phải:
Nhức đầu chỉ để show 1 cái giá nhảy.
Thực ra SSI WebSocket không có CORS (mình cũng vừa phát hiện khi build realtime v2). Nghĩa là browser gọi thẳng được. Vấn đề chỉ còn là: API phải đủ gọn để dev cảm thấy "đáng làm".
v1.3 dùng EventEmitter pattern:
"use client";
import { useEffect, useState } from "react";
import { realtime } from "vnstock-js";
export function LivePrice() {
const [price, setPrice] = useState<number | null>(null);
useEffect(() => {
const client = realtime.create({ symbols: ["FPT"] });
client.on("quote", (q) => {
if (q.symbol === "FPT") setPrice(q.matched.price);
});
client.connect();
return () => client.disconnect();
}, []);
return (
<div>FPT: {price != null ? (price * 1000).toLocaleString() : "..."}</div>
);
}
Đó là toàn bộ setup. Không backend, không proxy, không build config đặc biệt.
Watchlist thực tế thay đổi liên tục (user thêm/xoá mã). Reconnect mỗi lần là lãng phí:
client.subscribe(["VNM"]); // thêm mã mới vào sub hiện tại
client.unsubscribe(["FPT"]); // bỏ mã khỏi sub
Socket giữ nguyên, chỉ gửi message sub/unsub tương ứng.
v1.0 realtime dùng WebSocket ping() để giữ kết nối. Bug: browser không expose ping() API nên về bản chất là no-op, mà server lại reset connection mỗi ~40s vì tưởng client chết.
v1.3 bỏ ping, dùng dead-man's switch: nếu trong deadManTimeout (mặc định 60s) không nhận bất kỳ message nào, coi như chết và reconnect. Đơn giản hơn, đúng hơn, chạy ở cả Node lẫn browser.
Xem docs realtime đầy đủ có luôn ví dụ chỉ connect trong giờ giao dịch và hiển thị trạng thái kết nối.
await init() bắt buộcTừ v1.3, data symbols và holidays không còn bundle trong npm package — fetch runtime từ raw GitHub, cache disk 24h. Lợi: update symbols mới không cần bump version, package nhẹ hơn. Cost: gọi init() một lần ở entrypoint:
import { init } from "vnstock-js";
await init(); // gọi 1 lần ở app start
Các API truy vấn trực tiếp (quote, history, company, financials) vẫn không cần init(). Chỉ bắt buộc khi dùng stock.search, listing.*, market.calendar.*.
Sau 1 tuần dùng CLI, mình tìm ra kha khá bug nhỏ, fix gọn trong v1.3.1:
--range 7d giờ trả đúng ~7 phiên (trước đây trả ~365 do VCI API bỏ qua start)56.291760000000004k)--exchange HOSE hoạt động (auto map HOSE → HSX)symbols mặc định trả đầy đủ, --limit chỉ khi cầnvnstock -v cho versionFeedback vẫn mở ở GitHub Issues. Cám ơn mọi người đã dùng và góp ý ❤️