Logo FinLensFinLens Docs

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êNgân hàng Nhà nước; 3.348 chuỗi chỉ tiêu, cập nhật hằng ngày.

GọiTrả 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:

  1. unit là 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ạnh USD/thùng.
  2. date là CUỐI KỲ QUAN SÁT, không phải một phiên giao dịch. Cột period đi kèm mới nói kỳ nào.
  3. 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      VND

Nê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ểuMặc địnhNội dung
topicMacroTopicArgNone"fdi", "fdi,cpi" hoặc ["fdi", "cpi"]. None là cả 24 chủ đề.
freqMacroFreqArgNone"daily" · "monthly" · "quarterly" · "yearly". None là mọi tần suất.
refreshboolFalseBỏ qua bản trong cache và lấy lại từ máy chủ.
on_errorOnError"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ộtdtypeCó thể nullNội dung
codestringkhôngMã chỉ tiêu, chữ thường. Đây là giá trị đưa vào series().
namestringkhôngTên chỉ tiêu theo nguồn, tiếng Việt.
topicstringkhôngChủ đề — 24 giá trị.
frequencystringkhôngdaily · monthly · quarterly · yearly.
unitstringĐơn vị, nhãn của nguồn.
is_cumulativeboolkhôngTrue khi con số là luỹ kế từ đầu năm.
point_countInt64khôngSố điểm dữ liệu, kể cả điểm rỗng.
non_null_countInt64khôngSố điểm có giá trị.
first_datedatetime64[ns]Kỳ quan sát đầu tiên.
last_datedatetime64[ns]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ấtSố chuỗiVí dụ
daily107Tỷ 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ô
monthly2.901CPI, xuất nhập khẩu, FDI, sản xuất công nghiệp
quarterly154GDP, cán cân thanh toán
yearly186Cá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",
) -> DataFrame

codes 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ểuMặc địnhNội dung
codesMacroCodesArg"ty_gia_trung_tam_daily", nhiều mã cách nhau dấu phẩy, hoặc một danh sách.
startDateArgNoneNgày bắt đầu, tính cả ngày này. None là từ điểm đầu tiên.
endDateArgNoneNgày kết thúc, tính cả ngày này. None là tới điểm cuối.
refreshboolFalseBỏ qua bản trong cache.
on_errorOnError"warn"warn · raise · ignore.

Bỏ trống startend 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ộtdtypeCó thể nullNội dung
codestringkhôngMã chỉ tiêu, chữ thường.
namestringkhôngTên chỉ tiêu, tiếng Việt.
frequencystringkhôngdaily · monthly · quarterly · yearly.
periodstringkhôngNhãn kỳ: "2026-08-05" · "7-2026" · "Q1-2026" · "2026".
datedatetime64[ns]khôngCuối kỳ quan sát, naive.
valuefloat64Giá trị, ở đúng đơn vị mà cột unit cùng dòng khai.
unitstringĐơn vị của dòng này.
is_cumulativeboolkhôngTrue 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:

NamespaceCộtNghĩaMúi giờ
client.eod.*dateMột phiên giao dịchnaive
client.intraday.*timeMột thời điểmAsia/Ho_Chi_Minh
client.macro.*dateCuối kỳ quan sátnaive

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="%"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ểuMặc địnhNội dung
kindOmoKindArgNoneMột hoặc nhiều nhóm nghiệp vụ. None là cả sáu.
start / endDateArgNoneBiên đóng cả hai đầu.
freqOmoFreqArg"daily""daily" · "weekly" · "monthly".
refreshboolFalseBỏ qua bản trong cache.
on_errorOnError"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

kindNội dungunit
policy_rateLãi suất điều hành của NHNN — tái chiết khấu, tái cấp vốn%
interbank_rateLãi suất bình quân liên ngân hàng theo kỳ hạn%
interbank_turnoverDoanh số giao dịch liên ngân hàng theo kỳ hạnVND
reverse_repoMua kỳ hạn giấy tờ có giá — nghiệp vụ bơm tiềnVND
billTín phiếu NHNN — nghiệp vụ hút tiềnVND
net_pumpBơm hút ròngVND

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_repobill, 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ộtdtypeCó thể nullNội dung
kindstringkhôngNhóm nghiệp vụ.
namestringkhôngTên dòng số liệu theo nguồn.
directionstringPhát hành · đáo hạn · lưu hành · ròng. null ở nhóm lãi suất và doanh số.
tenorstringKỳ hạn. Chỉ có ở hai nhóm liên ngân hàng.
frequencystringkhôngMức tổng hợp của dòng.
datedatetime64[ns]khôngCuối kỳ quan sát, naive.
valuefloat64Giá trị, ở đơn vị mà unit cùng dòng khai.
unitstringVND 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ểuMặc địnhNội dung
flowTradeFlowArg"export""export" · "import" · "balance".
byTradeBreakdownArg"total""total" · "country" · "product".
start / endDateArgNoneBiên đóng cả hai đầu.
freqTradeFreqArg"monthly""monthly" · "yearly".
refreshboolFalseBỏ qua bản trong cache.
on_errorOnError"warn"warn · raise · ignore.

Cột trả về

CộtdtypeCó thể nullNội dung
flowstringkhôngexport · import · balance.
partnerstringĐối tác. Chỉ có giá trị khi by="country".
productstringNhóm mặt hàng. Chỉ có giá trị khi by="product".
periodstringkhông"7-2026" hoặc "2026".
datedatetime64[ns]khôngCuối kỳ quan sát, naive.
valuefloat64Kim ngạch. Âm với flow="balance" nghĩa là nhập siêu.
unitstringkhô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ọiThờ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

Nội dung trang