Tra cứu danh mục mã chứng khoán Việt Nam bằng Python
Dùng finlens lấy danh sách mã chứng khoán HOSE, HNX, UPCOM, cây ngành ICB và danh mục chứng quyền có bảo đảm về pandas DataFrame, kèm bộ lọc theo sàn và ngành.
Trước khi hỏi giá của một mã, người ta hỏi có những mã nào. Ba phương thức
trong client.meta trả lời đúng ba câu hỏi mở đầu đó, và tất cả đều trả về
pandas.DataFrame.
| Gọi | Trả lời câu hỏi |
|---|---|
client.meta.symbols() | Thị trường có những mã nào, mã nào thuộc ngành nào? |
client.meta.sectors() | Mã ngành ICB nào tồn tại, tên tiếng Việt là gì? |
client.meta.warrants() | Chứng quyền nào đang niêm yết trên mã cơ sở nào? |
Đây là danh mục, không phải số liệu: không cột nào ở đây mang đơn vị, trừ
exercise_price của chứng quyền.
import finlens
client = finlens.client()
tickers = client.meta.symbols(exchange="HOSE")["symbol"].tolist()
df = client.eod.stock.ohlcv(tickers[:50], start="2026-01-01")df["symbol"].tolist() là cầu nối giữa trang này và mọi trang còn lại: lấy danh
sách mã ở đây rồi đưa thẳng sang client.eod,
client.intraday hay
client.financials.
Không phương thức nào ở đây có tham số vị trí
Khác client.eod.* (nơi symbols hoặc icb là tham số vị trí), cả ba phương
thức của client.meta đều keyword-only từ đầu. Đầu vào của chúng là bộ
lọc, không phải một danh sách mã cần lấy dữ liệu — mà một bộ lọc bỏ trống được
thì không có lý do gì để đứng ở vị trí đầu tiên.
client.meta.symbols(exchange="HOSE") # đúng
client.meta.symbols("HOSE") # TypeErrorBỏ trống hết bộ lọc là chuyện bình thường và là lời gọi thường gặp nhất:
client.meta.symbols() trả về trọn danh mục.
symbols() — danh mục mã và chứng chỉ quỹ
client.meta.symbols(
*,
exchange=None,
icb=None,
kind=None,
company_type=None,
refresh=False,
on_error="warn",
) -> DataFrameBốn bộ lọc cộng dồn với nhau: khai cả exchange lẫn icb là hỏi giao của
hai điều kiện, không phải hợp.
| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
exchange | ExchangeArg | None | "HOSE" · "HNX" · "UPCOM". Không phân biệt hoa thường. None là cả ba sàn. |
icb | IcbFilterArg | None | "8300", "8300,8355" hoặc ["8300", "8355"]. Nhận cả mã cấp 2 lẫn mã cấp 4. None là mọi ngành. |
kind | SymbolKindArg | None | "stock" cổ phiếu · "fund" chứng chỉ quỹ. None là cả hai. |
company_type | CompanyTypeArg | None | "CT" · "NH" · "CK" · "BH". None là mọi loại hình. |
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. |
exchange là tên sàn giao dịch, không phải tên chỉ số. "VNINDEX" là mã
chỉ số của HOSE và nó nhận một lỗi 400 ở đây chứ không phải một bảng rỗng —
kiểu ExchangeArg biến lỗi đó thành một gạch đỏ ngay trong IDE.
Cột trả về
| Cột | dtype | Có thể null | Nội dung |
|---|---|---|---|
symbol | string | không | Mã chứng khoán, viết hoa. Ví dụ HPG. |
exchange | string | có | Sàn niêm yết: HOSE · HNX · UPCOM. |
kind | string | không | stock cổ phiếu · fund chứng chỉ quỹ. |
company_type | string | có | CT phi tài chính · NH ngân hàng · CK chứng khoán · BH bảo hiểm · QU quỹ. |
icb_level4 | string | có | Mã ngành ICB cấp 4. Ví dụ 8355. |
icb_name4 | string | có | Tên ngành cấp 4, tiếng Việt. |
icb_level2 | string | có | Mã ngành ICB cấp 2. Ví dụ 8300. |
icb_name2 | string | có | Tên ngành cấp 2, tiếng Việt. |
name | string | có | Tên đầy đủ của tổ chức phát hành. |
short_name | string | có | Tên gọi tắt. Ví dụ Hòa Phát. |
Mười cột luôn có mặt đúng dtype kể cả khi bộ lọc không khớp dòng nào, nên
bạn không cần bọc if not df.empty quanh lời gọi. Frame sắp tăng dần theo
symbol.
icb_level4 mang MÃ ngành, không phải con số cấp
icb_level4 == "8355" nghĩa là mã ngành cấp 4 của mã này là 8355 — không
phải "cấp của nó bằng 8355". Đọc tên cột như icb_code_level4 thì không bao giờ
nhầm.
Cùng lý do đó, mã ngành luôn là chuỗi. 8355 viết dạng số làm mất số 0
đứng đầu ở những mã như "0533".
Mỗi dòng mang sẵn cả hai cấp ngành
icb_level4 là cấp được gán trực tiếp trong dữ liệu; icb_level2 đã được máy
chủ suy ra qua chuỗi cha và đặt sẵn trên mỗi dòng. Nhờ vậy bạn gộp theo
ngành lớn bằng một phép groupby, không phải gọi thêm sectors() rồi tự nối
cây:
df = client.meta.symbols()
df.groupby("icb_name2")["symbol"].count().sort_values(ascending=False)Và vì bộ lọc icb nhận cả hai cấp, bạn hỏi được ngành lớn mà không cần biết nó
gồm những ngành con nào:
ngan_hang = client.meta.symbols(icb="8300") # mã cấp 2 — cả ngành ngân hàng
chi_tiet = client.meta.symbols(icb="8355") # mã cấp 4Máy chủ tra cấp giúp bạn, nên không có tham số icb_level ở phương thức
này để phải nhớ. Mã thuộc cấp 1 hoặc cấp 3 nhận 400 kèm giải thích, chứ
không phải một bảng rỗng. Không nhớ mã thì tra bằng client.meta.sectors() ở
mục dưới.
Chứng chỉ quỹ lọc bằng `kind`, không bằng `company_type`
Hai tham số này trông giống nhau nhưng trả lời hai câu hỏi khác hẳn:
kindnói đây là loại tài sản gì — cổ phiếu hay chứng chỉ quỹ niêm yết.company_typenói cây khoản mục báo cáo tài chính nào áp cho mã này.
Chứng chỉ quỹ xuất hiện ở cột company_type với giá trị QU, nhưng "QU"
cố ý không nằm trong kiểu CompanyTypeArg — quỹ không có báo cáo tài chính
dạng cây nào, nên company_type="QU" là một gạch đỏ lúc gõ code chứ không phải
một lỗi sau một vòng gọi mạng.
quy = client.meta.symbols(kind="fund") # đúng
quy = client.meta.symbols(company_type="QU") # saiSố chứng chỉ quỹ niêm yết hiện đếm được hai chữ số và tất cả đều trên HOSE.
# Ngân hàng trên HOSE, kèm tên gọi tắt
nh = client.meta.symbols(exchange="HOSE", company_type="NH")
nh[["symbol", "short_name", "icb_name4"]]
# Mọi mã chứng khoán (không gồm chứng chỉ quỹ) trên UPCOM
upcom = client.meta.symbols(exchange="UPCOM", kind="stock")sectors() — cây ngành ICB
client.meta.sectors(
*,
level=None,
refresh=False,
on_error="warn",
) -> DataFrameNguồn duy nhất của mã ngành cho mọi tham số icb trong thư viện — kể cả
client.eod.sector.ohlcv() và client.eod.sector.investor.flow(). Một bảng
ngành chép tay sẽ đúng lúc bạn chép và sai vào một lúc nào đó bạn không biết;
lời gọi này thì không.
| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
level | IcbLevelArg | None | 2 ngành lớn hoặc 4 ngành chi tiết. None trả cả hai cấp. |
refresh | bool | False | Bỏ qua cache. |
on_error | OnError | "warn" | warn · raise · ignore. |
| Cột | dtype | Có thể null | Nội dung |
|---|---|---|---|
icb | string | không | Mã ngành ICB, chuỗi số. Ví dụ 8355. |
level | int8 | không | Cấp trong cây: 2 hoặc 4. |
name_vi | string | có | Tên ngành, tiếng Việt. |
parent_icb | string | có | Mã ngành cha, null ở gốc cây. |
Frame sắp tăng dần theo level rồi icb, nên một lời gọi mang cả hai cấp đọc
ra được là một cái cây chứ không phải một danh sách phẳng trộn lẫn.
Không có cột tên tiếng Anh. name_vi là tên duy nhất, và đó là trạng thái
vĩnh viễn chứ không phải một khoảng trống tạm thời — phát một cột toàn null
để bạn tự đoán thì tệ hơn không phát.
nganh = client.meta.sectors(level=2)
len(nganh)
# 19
# Dựng lại cây bằng chính bảng này
ca_cay = client.meta.sectors()
con_cua = ca_cay.groupby("parent_icb")["icb"].apply(list)Chỉ hai cấp được phục vụ. Cấp 1 và cấp 3 có tồn tại trong chuẩn ICB nhưng
không cấp nào trong hai cấp đó có dữ liệu tổng hợp, nên chúng nhận 400 chứ
không trả một bảng rỗng — và kiểu IcbLevelArg chặn chúng ngay lúc gõ code.
Cha của một ngành cấp 4 là ngành CẤP 3, không phải cấp 2
Cây ICB có đủ bốn cấp; lời gọi này chỉ trả về hai cấp được phục vụ. Nghĩa là
parent_icb của một dòng cấp 4 trỏ tới một mã không có mặt trong chính bảng
này, và phép ghép sau đây sẽ mất gần hết dòng mà không báo lỗi nào:
cap4 = ca_cay[ca_cay["level"] == 4]
cap2 = ca_cay[ca_cay["level"] == 2]
cap4.merge(cap2, left_on="parent_icb", right_on="icb") # sai — ghép hụtMuốn ngành cấp 2 của một mã chứng khoán thì đọc cột icb_level2 mà
client.meta.symbols() trả về, nơi chuỗi cha đã được nối sẵn.
`level` là số nguyên; mã ngành là chuỗi
level=2 là cấp — một số nguyên. icb="8300" là mã ngành — một chuỗi.
Hai thứ đi qua hai tham số khác nhau và không đổi chỗ cho nhau được; kiểu khác
nhau chính là thứ giữ cho việc nhầm lẫn không diễn ra trong im lặng.
Thêm một chỗ dễ vấp: thông báo lỗi từ máy chủ có thể gọi tham số này là
icb_level. Trong Python thì tên nó là level — gõ icb_level=2 nhận
TypeError ngay lập tức chứ không âm thầm bỏ qua bộ lọc.
warrants() — danh mục chứng quyền có bảo đảm
client.meta.warrants(
*,
underlying=None,
active_only=True,
refresh=False,
on_error="warn",
) -> DataFrameChứng quyền nào đang niêm yết trên mã cơ sở nào, kèm điều khoản hợp đồng.
| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
underlying | UnderlyingArg | None | Mã cơ sở: "HPG", "HPG,FPT" hoặc ["HPG", "FPT"]. None là mọi chứng quyền. |
active_only | bool | True | True chỉ trả mã chưa đáo hạn; False trả cả mã đã hết hạn. |
refresh | bool | False | Bỏ qua cache. |
on_error | OnError | "warn" | warn · raise · ignore. |
underlying là mã của cổ phiếu cơ sở, không phải mã của chứng quyền. Mã
chứng quyền (CHPG2525) nằm ở cột symbol của kết quả.
| Cột | dtype | Có thể null | Đơn vị | Nội dung |
|---|---|---|---|---|
symbol | string | không | — | Mã chứng quyền. Ví dụ CHPG2525. |
underlying | string | có | — | Mã cơ sở. Ví dụ HPG. |
issuer | string | có | — | Mã công ty chứng khoán phát hành. Ví dụ SSI, VND. |
exercise_price | float64 | có | VND thô | Giá thực hiện. |
maturity_date | datetime64[ns] | có | — | Ngày đáo hạn, không mang múi giờ. |
`exercise_price` là VND thô, không phải nghìn VND
Cộng thẳng exercise_price với cột close của client.eod.stock.ohlcv() là
sai 1.000 lần — và không có exception nào được ném. Kiểm bằng một dòng thay
vì giả định:
cq = client.meta.warrants(underlying="HPG")
cq.attrs["finlens"]["units"]["exercise_price"]
# 'VND'price_basis của bảng này là null chứ không phải raw: giá thực hiện là một
điều khoản hợp đồng, không phải một mức giá quan sát được trên thị trường,
nên câu hỏi "đã điều chỉnh quyền chưa" không áp cho nó.
active_only=True giữ lại những mã có ngày đáo hạn từ hôm nay trở đi theo
giờ Việt Nam — chứng quyền đáo hạn đúng hôm nay vẫn còn trong bảng. maturity_date
là tín hiệu duy nhất cho biết một chứng quyền còn hạn, nên đừng tìm một cột
trạng thái nào khác.
# Chứng quyền còn hạn trên HPG
cq = client.meta.warrants(underlying="HPG")
cq[["symbol", "issuer", "exercise_price", "maturity_date"]]
# Tra một mã đã hết hạn gặp trong dữ liệu lịch sử
cu = client.meta.warrants(active_only=False)
df = client.eod.warrant.ohlcv(cu["symbol"].tolist()[:5])Lời gọi này chỉ trả danh mục. Giá và khối lượng giao dịch của chính chứng
quyền nằm ở client.eod.warrant.ohlcv() — xem
Dữ liệu cuối ngày.
Độ phủ — và cách tự đếm lại
Danh mục hiện có hơn 1.600 mã trên cả ba sàn: UPCOM đông nhất, rồi tới HOSE, rồi HNX. Cây ngành có 19 ngành cấp 2 và khoảng một trăm ngành cấp 4.
Những con số này đổi theo từng đợt niêm yết mới, nên đừng chép chúng vào code — đếm lại bằng chính thư viện:
df = client.meta.symbols()
len(df) # tổng số mã
df["exchange"].value_counts() # phân bố theo sàn
df["kind"].value_counts() # cổ phiếu và chứng chỉ quỹ
len(client.meta.sectors(level=4)) # số ngành chi tiết
len(client.meta.warrants()) # chứng quyền còn hạnDanh mục lớn hơn `max_rows` của gói thì bị cắt
Một lời gọi symbols() là một request và trả về cả danh mục trong một
frame. Nếu gói dịch vụ của bạn có max_rows nhỏ hơn số dòng của danh mục, kết
quả bị cắt ngắn kèm cảnh báo và df.attrs["finlens"]["truncated"] bằng True:
df.attrs["finlens"]["truncated"] # True nghĩa là bảng chưa đầy đủ
client.limits()["max_rows"] # hạn mức hiệu lực của khoá bạn đang dùngThu hẹp bằng bộ lọc (exchange=, icb=) thay vì phân trang thủ công.
Ngược lại, danh mục không bị kẹp theo độ sâu lịch sử (history_days) của
gói: ở đây không có một con số giá nào, nên nó không phải thứ mà giới hạn ấy
sinh ra để bảo vệ.
Cache 24 giờ
Kết quả của cả ba phương thức được cache 24 giờ — danh mục đổi theo tháng
chứ không theo phiên. Dùng refresh=True khi bạn biết vừa có mã mới niêm yết:
df = client.meta.symbols(refresh=True)`as_of` ở đây là lúc BẠN hỏi, không phải mốc nước của dữ liệu
Với dữ liệu giá, df.attrs["finlens"]["as_of"] cho biết dữ liệu mới tới đâu.
Với ba bảng danh mục thì không: không có cách nào biết danh mục vừa đổi lúc nào,
nên "lúc bạn hỏi" là câu trả lời trung thực duy nhất.
Đừng đọc nó là "danh mục vừa cập nhật lúc đó" — nó chỉ nói rằng ta không biết.
Bản async
Cả ba phương thức đều có bản bất đồng bộ với cùng chữ ký:
import asyncio
import finlens
async def main():
async with finlens.AsyncClient() as client:
df = await client.meta.symbols(exchange="HOSE")
tickers = df["symbol"].tolist()
asyncio.run(main())Xem tiếp
Dữ liệu cuối ngày (EOD)
Đưa danh sách mã vừa lấy sang ohlcv(), và dùng mã ngành ICB cho chỉ số
ngành.
Báo cáo tài chính
company_type ở đây quyết định cây khoản mục nào áp cho mã.
Kiểu dữ liệu
ExchangeArg, SymbolKindArg, IcbLevelArg và nội dung
df.attrs["finlens"].
FinLens MCP
Hỏi thẳng "ngành ngân hàng có những mã nào" bằng tiếng Việt qua trợ lý AI.
Câu hỏi thường gặp
Làm sao lấy danh sách toàn bộ mã cổ phiếu đang niêm yết trên HOSE bằng Python?
Gọi `client.meta.symbols(exchange="HOSE")` của thư viện Python finlens rồi lấy cột `symbol` ra danh sách Python bằng `df["symbol"].tolist()`. Tham số `exchange` nhận `"HOSE"`, `"HNX"` hoặc `"UPCOM"`; bỏ trống thì trả về cả ba sàn. Đây là tên sàn giao dịch chứ không phải tên chỉ số, nên `exchange="VNINDEX"` nhận một lỗi 400. Cả ba phương thức của `client.meta` đều keyword-only, nên `client.meta.symbols("HOSE")` ném `TypeError`.
Tra mã ngành ICB của cổ phiếu Việt Nam ở đâu?
Gọi `client.meta.sectors()` của thư viện Python finlens để lấy cây ngành ICB — mỗi dòng gồm mã ngành `icb`, cấp `level`, tên tiếng Việt `name_vi` và mã ngành cha `parent_icb`. Truyền `level=2` cho ngành lớn hoặc `level=4` cho ngành chi tiết; bỏ trống thì trả cả hai cấp. Chỉ hai cấp này được phục vụ: cấp 1 và cấp 3 có trong chuẩn ICB nhưng không có dữ liệu tổng hợp nên nhận lỗi 400 chứ không trả bảng rỗng. Đây là nguồn mã ngành cho mọi tham số `icb` trong thư viện.
Lọc cổ phiếu theo ngành trong finlens phải dùng mã ngành cấp mấy?
Tham số `icb` của `client.meta.symbols()` nhận cả mã cấp 2 lẫn mã cấp 4 trong cùng một tham số, nên bạn không cần biết trước mã mình đang cầm thuộc cấp nào — `icb="8300"` (ngành ngân hàng, cấp 2) và `icb="8355"` (cấp 4) đều chạy, và phương thức này không có tham số `icb_level` để phải nhớ. Mã ngành luôn là chuỗi chứ không phải số, vì ép sang số sẽ làm mất số 0 đứng đầu ở những mã như `"0533"`. Mỗi dòng kết quả mang sẵn cả `icb_level4` lẫn `icb_level2` nên gộp theo ngành lớn chỉ cần một phép `groupby`.
Làm sao biết chứng quyền nào còn hạn và gắn với mã cơ sở nào?
Gọi `client.meta.warrants(underlying="HPG")` của thư viện Python finlens để lấy chứng quyền trên một mã cơ sở, hoặc bỏ trống tham số để lấy toàn bộ. Mặc định `active_only=True` chỉ trả những mã có ngày đáo hạn từ hôm nay trở đi theo giờ Việt Nam; đặt `active_only=False` khi cần tra một mã cũ gặp trong dữ liệu lịch sử. Frame gồm `symbol`, `underlying`, `issuer`, `exercise_price` và `maturity_date`. Cột `exercise_price` tính bằng VND thô, không phải nghìn VND như giá cổ phiếu, nên cộng thẳng nó với cột `close` là sai 1.000 lần.
Lọc chứng chỉ quỹ niêm yết bằng tham số nào?
Dùng `client.meta.symbols(kind="fund")` của thư viện Python finlens, không dùng `company_type`. Cột `kind` nhận `"stock"` cho cổ phiếu và `"fund"` cho chứng chỉ quỹ niêm yết; đây là trường duy nhất phân biệt được hai loại tài sản. Tham số `company_type` phục vụ việc khác: nó nói cây khoản mục báo cáo tài chính nào áp cho mã, với bốn giá trị `"CT"`, `"NH"`, `"CK"`, `"BH"` — loại `"QU"` (quỹ) cố ý không nằm trong từ vựng của nó vì quỹ không có báo cáo tài chính dạng cây.
Cập nhật lần cuối