Lấy dữ liệu vĩ mô Việt Nam bằng Python
Dùng finlens lấy CPI, tỷ giá, lãi suất liên ngân hàng, lợi suất trái phiếu chính phủ, nghiệp vụ thị trường mở của NHNN và số liệu xuất nhập khẩu về pandas DataFrame.
Giá cổ phiếu không tự giải thích được chính nó. client.macro đưa CPI, tỷ giá,
lãi suất, thanh khoản hệ thống và cán cân thương mại về cùng một DataFrame với
dữ liệu thị trường, để bạn không phải đi lấy chỗ khác rồi tự ghép.
Nguồn là Tổng cục Thống kê và Ngân hàng Nhà nước; 3.348 chuỗi chỉ tiêu, cập nhật hằng ngày.
| Gọi | Trả lời câu hỏi |
|---|---|
client.macro.indicators() | Có những chỉ tiêu nào, mã của chúng là gì? |
client.macro.series() | Số liệu của chỉ tiêu này qua các kỳ? |
client.macro.omo() | NHNN bơm hay hút tiền, lãi suất liên ngân hàng bao nhiêu? |
client.macro.trade() | Xuất nhập khẩu với ai, mặt hàng gì, cán cân ra sao? |
import finlens
client = finlens.client()
ds = client.macro.indicators(topic="cpi", freq="monthly")
df = client.macro.series(ds["code"].tolist()[:5])Ba thứ ở đây khác mọi namespace còn lại
Vĩ mô là namespace đầu tiên không khoá theo mã chứng khoán. Khoá của nó là
(chỉ tiêu, tần suất, thời gian), và ba hệ quả dưới đây sẽ làm bạn đọc sai số
nếu bỏ qua:
unitlà một CỘT, không phải thuộc tính của cả bảng. Một lời gọi hợp lệ trả về%nằm cạnhUSD/thùng.datelà CUỐI KỲ QUAN SÁT, không phải một phiên giao dịch. Cộtperiodđi kèm mới nói kỳ nào.- Tần suất là một phần danh tính của chuỗi. Chuỗi quý và chuỗi năm của cùng một chỉ tiêu là hai mã khác nhau.
Đơn vị nằm ở từng dòng
Mọi namespace khác của finlens khai đơn vị ở df.attrs["finlens"]["units"],
và điều đó đúng vì một lời gọi không bao giờ trộn đơn vị: cổ phiếu và phái sinh
nằm ở hai namespace khác nhau nên một bảng không thể vừa mang nghìn VND vừa mang
điểm chỉ số.
Vĩ mô không có ranh giới nào như thế. Hỏi hai chỉ tiêu bất kỳ là một câu hỏi hoàn toàn bình thường:
df = client.macro.series(["gia_vang_giao_ngay_daily", "ty_gia_trung_tam_daily"])
df.groupby("code")["unit"].first()
# gia_vang_giao_ngay_daily USD/Ounce
# ty_gia_trung_tam_daily VNDNên unit là một cột, và bạn đọc nó theo từng dòng. Tách bao nhiêu namespace
cũng không đưa một bảng vĩ mô về một đơn vị duy nhất.
Đơn vị ở đây là nhãn của nguồn, không thuộc từ vựng của thư viện
Dữ liệu thị trường dùng một tập đơn vị đóng: kVND, VND, mVND,
index_point, share, contract, ratio, pct. Vĩ mô thì không — nó phát
lại đúng nhãn mà cơ quan thống kê dùng, dạng chuỗi tự do:
% · VND · USD · Tấn · Dự án · USD/thùng · VND/lượng · USD/Ounce
· Triệu USD · Tỷ VND · Lần
Hệ quả: df.finlens.to_vnd() không quy đổi được chúng và cũng không thử. So
sánh hai chuỗi khác đơn vị là việc của bạn, không phải của thư viện.
indicators() — tra mã trước khi hỏi số
client.macro.indicators(
*,
topic=None,
freq=None,
refresh=False,
on_error="warn",
) -> DataFrame| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
topic | MacroTopicArg | None | "fdi", "fdi,cpi" hoặc ["fdi", "cpi"]. None là cả 24 chủ đề. |
freq | MacroFreqArg | None | "daily" · "monthly" · "quarterly" · "yearly". None là mọi tần suất. |
refresh | bool | False | Bỏ qua bản trong cache và lấy lại từ máy chủ. |
on_error | OnError | "warn" | warn · raise · ignore. |
Giá trị hợp lệ của topic chính là cột topic của kết quả — gọi không kèm bộ
lọc một lần để xem có những chủ đề nào.
Cột trả về
| Cột | dtype | Có thể null | Nội dung |
|---|---|---|---|
code | string | không | Mã chỉ tiêu, chữ thường. Đây là giá trị đưa vào series(). |
name | string | không | Tên chỉ tiêu theo nguồn, tiếng Việt. |
topic | string | không | Chủ đề — 24 giá trị. |
frequency | string | không | daily · monthly · quarterly · yearly. |
unit | string | có | Đơn vị, nhãn của nguồn. |
is_cumulative | bool | không | True khi con số là luỹ kế từ đầu năm. |
point_count | Int64 | không | Số điểm dữ liệu, kể cả điểm rỗng. |
non_null_count | Int64 | không | Số điểm có giá trị. |
first_date | datetime64[ns] | có | Kỳ quan sát đầu tiên. |
last_date | datetime64[ns] | có | Kỳ quan sát cuối cùng. |
Phân bố theo tần suất, đo ngày 06/08/2026:
| Tần suất | Số chuỗi | Ví dụ |
|---|---|---|
daily | 107 | Tỷ giá trung tâm, lãi suất liên ngân hàng, lợi suất TPCP, giá vàng, dầu thô |
monthly | 2.901 | CPI, xuất nhập khẩu, FDI, sản xuất công nghiệp |
quarterly | 154 | GDP, cán cân thanh toán |
yearly | 186 | Các chỉ tiêu tổng hợp năm |
Đừng dựng mã từ tên — hãy tra rồi lưu lại
Mã là một bản đồ tra cứu, không phải một hàm. Ba lý do, mỗi lý do đủ để một
slugify(name) phía bạn cho ra mã sai:
- Nguồn đổi tên chỉ tiêu mà mã thì không đổi — nên với chuỗi từng bị đổi tên, mã hiện hành được sinh từ một cái tên không còn tồn tại.
- Tên trùng nhau được khử bằng hậu tố thứ tự (
_2,_3) cấp theo thứ tự gieo, mà thứ tự ấy không nằm trong cái tên. - Bảng ánh xạ sửa tay được.
Chiều ngược lại thì hoàn toàn an toàn: mã được cấp một lần rồi thôi, nên lưu nó vào code hay cấu hình là chuyện đúng đắn.
last_date trả lời "chuỗi này phủ tới đâu", không phải "số mới nhất đã về
chưa": danh mục được cache 12 giờ. Cần câu trả lời kia thì hỏi thẳng series().
series() — số liệu
client.macro.series(
codes,
*,
start=None,
end=None,
refresh=False,
on_error="warn",
) -> DataFramecodes là tham số vị trí duy nhất của cả namespace này, vì nó là danh sách
khoá thật sự — ba phương thức còn lại chỉ mang bộ lọc nên đều keyword-only.
| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
codes | MacroCodesArg | — | "ty_gia_trung_tam_daily", nhiều mã cách nhau dấu phẩy, hoặc một danh sách. |
start | DateArg | None | Ngày bắt đầu, tính cả ngày này. None là từ điểm đầu tiên. |
end | DateArg | None | Ngày kết thúc, tính cả ngày này. None là tới điểm cuối. |
refresh | bool | False | Bỏ qua bản trong cache. |
on_error | OnError | "warn" | warn · raise · ignore. |
Bỏ trống start và end thì trả toàn bộ dải có dữ liệu. Không có mặc định
nào bị bịa ra ở đây — khác dữ liệu giá, nơi mặc định là một năm gần nhất — vì
một chuỗi vĩ mô có thể bắt đầu từ 2001 và cắt nó về một năm là bỏ mất phần lớn
thứ người ta đến để lấy.
Cột trả về
| Cột | dtype | Có thể null | Nội dung |
|---|---|---|---|
code | string | không | Mã chỉ tiêu, chữ thường. |
name | string | không | Tên chỉ tiêu, tiếng Việt. |
frequency | string | không | daily · monthly · quarterly · yearly. |
period | string | không | Nhãn kỳ: "2026-08-05" · "7-2026" · "Q1-2026" · "2026". |
date | datetime64[ns] | không | Cuối kỳ quan sát, naive. |
value | float64 | có | Giá trị, ở đúng đơn vị mà cột unit cùng dòng khai. |
unit | string | có | Đơn vị của dòng này. |
is_cumulative | bool | không | True khi con số là luỹ kế từ đầu năm. |
Frame ở dạng long và sắp tăng dần theo (code, date). Thêm một chỉ tiêu là
thêm dòng chứ không đổi schema.
date là cuối kỳ, period mới nói kỳ nào
Thư viện có ba loại cột thời gian, và đây là loại thứ ba:
| Namespace | Cột | Nghĩa | Múi giờ |
|---|---|---|---|
client.eod.* | date | Một phiên giao dịch | naive |
client.intraday.* | time | Một thời điểm | Asia/Ho_Chi_Minh |
client.macro.* | date | Cuối kỳ quan sát | naive |
2026-07-31 một mình không nói được đây là số của tháng 7 hay một quan sát ngày
rơi đúng vào 31/7 — nên cột period đi kèm và mang nhãn đọc được.
Luôn dùng date để nối chuỗi, đừng phân tích period.
Giá trị đi ra đúng bằng con số của nguồn
Không nhân hệ số nào, không đổi thang, không làm tròn. 5.4 với unit="%" là
5,4 phần trăm — không phải 540.
NaN là kỳ nguồn để trống, không phải số 0.
is_cumulative — cột dễ đọc sai nhất
2.135 trên 3.348 chuỗi mang giá trị luỹ kế từ đầu năm. Cùng một chỉ tiêu thường có cả hai dạng, và mã là thứ phân biệt:
df = client.macro.series([
"tong_xuat_khau_gia_tri_luy_ke_m_monthly", # cộng dồn từ đầu năm
"tong_xuat_khau_gia_tri_thang_m_monthly", # số của riêng tháng
])
df.groupby("code")["is_cumulative"].first()Hai mã trên cùng đơn vị (Triệu USD), cùng tần suất, cùng 115 kỳ — và khác nhau
đúng ở một cột.
Vẽ thẳng chuỗi luỹ kế lên biểu đồ thì đường sẽ leo suốt năm rồi rơi về gần 0 vào tháng 1. Đó là định nghĩa của nó, không phải một cú sụp.
Muốn số của riêng một kỳ thì lấy hiệu hai kỳ liên tiếp — nhưng chỉ với chuỗi
mang đơn vị tuyệt đối. Với chuỗi % dạng "luỹ kế so với cùng kỳ năm trước"
thì phép hiệu ấy vô nghĩa.
Tần suất là danh tính, không phải cách xem
70 chỉ tiêu thuộc nhóm Cán cân thanh toán tồn tại ở cả quý lẫn năm. Chúng là hai mã khác nhau mang hai bộ số khác nhau — chuỗi năm không phải chuỗi quý cộng lại. Đó là lý do mọi mã đều kết thúc bằng tần suất của nó.
omo() — nghiệp vụ thị trường mở
client.macro.omo(
*,
kind=None,
start=None,
end=None,
freq="daily",
refresh=False,
on_error="warn",
) -> DataFrame| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
kind | OmoKindArg | None | Một hoặc nhiều nhóm nghiệp vụ. None là cả sáu. |
start / end | DateArg | None | Biên đóng cả hai đầu. |
freq | OmoFreqArg | "daily" | "daily" · "weekly" · "monthly". |
refresh | bool | False | Bỏ qua bản trong cache. |
on_error | OnError | "warn" | warn · raise · ignore. |
Khác freq của indicators(): ở đây nguồn phát sẵn cả ba mức và chúng nói về
cùng một nghiệp vụ, nên đây thật sự là một cách xem.
Sáu nhóm, hai vũ trụ đo
kind | Nội dung | unit |
|---|---|---|
policy_rate | Lãi suất điều hành của NHNN — tái chiết khấu, tái cấp vốn | % |
interbank_rate | Lãi suất bình quân liên ngân hàng theo kỳ hạn | % |
interbank_turnover | Doanh số giao dịch liên ngân hàng theo kỳ hạn | VND |
reverse_repo | Mua kỳ hạn giấy tờ có giá — nghiệp vụ bơm tiền | VND |
bill | Tín phiếu NHNN — nghiệp vụ hút tiền | VND |
net_pump | Bơm hút ròng | VND |
net_pump đã là số ròng, đừng cộng thêm
net_pump chính là kết quả ròng đã tính sẵn từ reverse_repo và bill,
không phải một thành phần thứ ba. Cộng cả ba nhóm lại là đếm đôi toàn bộ.
Và bốn nhóm tiền không cộng được với hai nhóm lãi suất. Lọc kind trước khi
tính bất cứ thứ gì:
tien = client.macro.omo(kind="reverse_repo,bill")
set(tien["unit"]) # {'VND'}Cột trả về
| Cột | dtype | Có thể null | Nội dung |
|---|---|---|---|
kind | string | không | Nhóm nghiệp vụ. |
name | string | không | Tên dòng số liệu theo nguồn. |
direction | string | có | Phát hành · đáo hạn · lưu hành · ròng. null ở nhóm lãi suất và doanh số. |
tenor | string | có | Kỳ hạn. Chỉ có ở hai nhóm liên ngân hàng. |
frequency | string | không | Mức tổng hợp của dòng. |
date | datetime64[ns] | không | Cuối kỳ quan sát, naive. |
value | float64 | có | Giá trị, ở đơn vị mà unit cùng dòng khai. |
unit | string | có | VND hoặc %. |
direction là bản máy đọc được của cột name — "KL phát hành Reverse Repo" so
với "KL đáo hạn Reverse Repo". Nó null ở các nhóm lãi suất và doanh số vì ở
đó câu hỏi "chiều nào" không có nội dung.
Ba chân của tín phiếu — phát hành, lưu hành, đáo hạn — kết thúc ở ba ngày khác nhau. Kết quả kèm một cảnh báo nói ngày cuối cùng thực sự có số liệu, suy từ chính bảng bạn vừa nhận.
trade() — xuất nhập khẩu
client.macro.trade(
*,
flow="export",
by="total",
start=None,
end=None,
freq="monthly",
refresh=False,
on_error="warn",
) -> DataFrame| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
flow | TradeFlowArg | "export" | "export" · "import" · "balance". |
by | TradeBreakdownArg | "total" | "total" · "country" · "product". |
start / end | DateArg | None | Biên đóng cả hai đầu. |
freq | TradeFreqArg | "monthly" | "monthly" · "yearly". |
refresh | bool | False | Bỏ qua bản trong cache. |
on_error | OnError | "warn" | warn · raise · ignore. |
Cột trả về
| Cột | dtype | Có thể null | Nội dung |
|---|---|---|---|
flow | string | không | export · import · balance. |
partner | string | có | Đối tác. Chỉ có giá trị khi by="country". |
product | string | có | Nhóm mặt hàng. Chỉ có giá trị khi by="product". |
period | string | không | "7-2026" hoặc "2026". |
date | datetime64[ns] | không | Cuối kỳ quan sát, naive. |
value | float64 | có | Kim ngạch. Âm với flow="balance" nghĩa là nhập siêu. |
unit | string | không | Đơn vị, nhãn của nguồn. |
Ba chiều bóc tách không cộng chéo với nhau
Mỗi lời gọi trả về đúng một chiều. Tổng theo by="country" bằng tổng theo
by="total", chứ không cộng thêm vào.
Nguồn có nhiều lớp tổng hợp lồng nhau — châu lục nằm cạnh quốc gia, mặt hàng cha nằm cạnh mặt hàng con — nên máy chủ lọc sẵn về đúng một lớp. Cộng dữ liệu thô của nguồn cho ra con số lệch tới +46%.
Ngoài ra flow="balance" chỉ tồn tại ở by="total". Tổ hợp không có nhận một
lỗi 400 kèm danh sách còn dùng được, chứ không phải một bảng rỗng.
freq="yearly" ở nguồn là luỹ kế từ đầu năm, không phải số của cả năm — máy
chủ lọc về đúng kỳ cuối cùng của mỗi năm và kèm một cảnh báo nói rõ điều đó.
Cache
| Lời gọi | Thời gian sống |
|---|---|
series() | 30 phút |
omo(freq="daily") | 30 phút |
omo(freq="weekly"/"monthly") | 12 giờ |
indicators() | 12 giờ |
trade() | 12 giờ |
series() giữ 30 phút kể cả khi bạn chỉ hỏi chuỗi tháng, và đó là lựa chọn
có chủ đích: thời gian sống được quyết định trước khi request rời khỏi máy bạn,
nên thứ duy nhất đọc được là danh sách mã — mà suy tần suất từ tên mã sẽ buộc
cache vào một quy ước đặt tên sửa tay được. Ngoài ra một lời gọi được phép trộn
tần suất, nên thời gian sống của cả bảng phải là của chuỗi tươi nhất trong đó.
Dùng refresh=True khi bạn biết một kỳ mới vừa được công bố.
Hạn mức
Dữ liệu vĩ mô không bị kẹp theo độ sâu lịch sử của gói dịch vụ — đây là thông tin công khai của cơ quan thống kê. Hạn mức số chỉ tiêu mỗi request và hạn mức request theo ngày vẫn áp bình thường; nhiều mã được chia thành nhiều request và ghép lại trong suốt, giống mọi lời gọi nhiều mã khác.
Một mã không tồn tại không làm hỏng cả lời gọi: nó vào phần lỗi theo từng mã
và các mã còn lại vẫn trả về, theo đúng on_error. Xem
Xử lý lỗi.
Câu hỏi thường gặp
Làm sao lấy chỉ số CPI của Việt Nam bằng Python?
Tra mã trước bằng `client.macro.indicators(topic="cpi")` của thư viện Python finlens, rồi truyền cột `code` sang `client.macro.series()`. Không tự đặt mã: mã chỉ tiêu là một bản đồ tra cứu chứ không phải một công thức — nguồn đổi tên thì mã giữ nguyên, tên trùng nhau được khử bằng hậu tố thứ tự, và bảng ánh xạ thì sửa tay được. Mỗi mã là một cặp (chỉ tiêu, tần suất) nên nó luôn kết thúc bằng tần suất, ví dụ `cpi_thuc_pham_so_voi_cung_ky_nam_truoc_m_monthly`.
Vì sao cột unit của dữ liệu vĩ mô nằm trong bảng chứ không phải trong metadata?
Vì một lời gọi hợp lệ có thể trả về nhiều đơn vị cùng lúc. `client.macro.series(["gia_vang_giao_ngay_daily", "ty_gia_trung_tam_daily"])` cho ra `USD/Ounce` nằm cạnh `VND` trong cùng một cột `value` — mọi namespace khác của finlens không bao giờ trộn đơn vị vì mỗi loại tài sản nằm ở một namespace riêng, còn vĩ mô thì không có ranh giới nào như thế. Đọc `unit` theo từng dòng trước khi cộng hay vẽ chung một trục. Đơn vị ở đây là nhãn chuỗi tự do của cơ quan thống kê nên `.finlens.to_vnd()` không quy đổi chúng.
Cột is_cumulative trong dữ liệu vĩ mô nghĩa là gì?
`is_cumulative=True` nghĩa là con số đã cộng dồn từ đầu năm chứ không phải số của riêng kỳ đó — 2.135 trên 3.348 chuỗi. Vẽ thẳng một chuỗi luỹ kế lên biểu đồ thì đường sẽ leo suốt năm rồi rơi về gần 0 vào tháng 1, và đó là cách nó được định nghĩa chứ không phải một cú sụp của nền kinh tế. Muốn số của riêng một kỳ thì lấy hiệu hai kỳ liên tiếp, nhưng chỉ với chuỗi mang đơn vị tuyệt đối; với chuỗi phần trăm dạng luỹ kế so với cùng kỳ năm trước thì phép hiệu ấy vô nghĩa.
Lấy số liệu bơm hút ròng của Ngân hàng Nhà nước bằng finlens thế nào?
Gọi `client.macro.omo(kind="net_pump")` của thư viện Python finlens. Sáu nhóm nghiệp vụ không cùng một vũ trụ đo: bốn nhóm mang tiền tính bằng VND và hai nhóm mang lãi suất tính bằng phần trăm, nên không lọc `kind` rồi cộng cột `value` là cộng lãi suất với tiền. Quan trọng hơn, `net_pump` chính là bơm hút ròng đã được tính sẵn từ mua kỳ hạn giấy tờ có giá và tín phiếu, không phải một thành phần thứ ba — cộng cả ba nhóm lại là đếm đôi toàn bộ.
Số liệu xuất nhập khẩu theo quốc gia và theo mặt hàng có cộng chung được không?
Không. Ba chiều bóc tách của `client.macro.trade()` — `by="total"`, `by="country"`, `by="product"` — nói về cùng một kim ngạch theo ba cách nhìn, nên tổng của chiều này bằng tổng của chiều kia chứ không cộng thêm vào. Nguồn có nhiều lớp tổng hợp lồng nhau, châu lục nằm cạnh quốc gia và mặt hàng cha nằm cạnh mặt hàng con, nên máy chủ lọc sẵn về đúng một lớp; cộng dữ liệu thô của nguồn cho ra con số lệch tới 46 phần trăm. Ngoài ra `flow="balance"` chỉ tồn tại ở `by="total"` và giá trị âm nghĩa là nhập siêu.
Mã chỉ tiêu vĩ mô của finlens có ổn định không, lưu lại được không?
Có, mã được cấp một lần rồi thôi. Nguồn đổi tên chỉ tiêu thì mã không đổi, vì người dùng đã lưu nó — nên lưu mã vào code hay cấu hình là an toàn. Chiều ngược lại thì không an toàn: đừng dựng mã từ cột `name`. Mã là một bản đồ tra cứu chứ không phải một hàm, và với chuỗi từng bị đổi tên thì mã hiện hành được sinh từ một cái tên không còn tồn tại. Luôn đọc mã từ `client.macro.indicators()`.
Cập nhật lần cuối