Logo FinLensFinLens Docs

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ọiTrả 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")              # TypeError

Bỏ 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",
) -> DataFrame

Bố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ểuMặc địnhNội dung
exchangeExchangeArgNone"HOSE" · "HNX" · "UPCOM". Không phân biệt hoa thường. None là cả ba sàn.
icbIcbFilterArgNone"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.
kindSymbolKindArgNone"stock" cổ phiếu · "fund" chứng chỉ quỹ. None là cả hai.
company_typeCompanyTypeArgNone"CT" · "NH" · "CK" · "BH". None là mọi loại hình.
refreshboolFalseBỏ qua bản trong cache và lấy lại từ máy chủ.
on_errorOnError"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ộtdtypeCó thể nullNội dung
symbolstringkhôngMã chứng khoán, viết hoa. Ví dụ HPG.
exchangestringSàn niêm yết: HOSE · HNX · UPCOM.
kindstringkhôngstock cổ phiếu · fund chứng chỉ quỹ.
company_typestringCT phi tài chính · NH ngân hàng · CK chứng khoán · BH bảo hiểm · QU quỹ.
icb_level4string ngành ICB cấp 4. Ví dụ 8355.
icb_name4stringTên ngành cấp 4, tiếng Việt.
icb_level2string ngành ICB cấp 2. Ví dụ 8300.
icb_name2stringTên ngành cấp 2, tiếng Việt.
namestringTên đầy đủ của tổ chức phát hành.
short_namestringTê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 4

Má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:

  • kind nói đây là loại tài sản gì — cổ phiếu hay chứng chỉ quỹ niêm yết.
  • company_type nó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")    # sai

Số 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",
) -> DataFrame

Nguồ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()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ểuMặc địnhNội dung
levelIcbLevelArgNone2 ngành lớn hoặc 4 ngành chi tiết. None trả cả hai cấp.
refreshboolFalseBỏ qua cache.
on_errorOnError"warn"warn · raise · ignore.
CộtdtypeCó thể nullNội dung
icbstringkhôngMã ngành ICB, chuỗi số. Ví dụ 8355.
levelint8khôngCấp trong cây: 2 hoặc 4.
name_vistringTên ngành, tiếng Việt.
parent_icbstringMã 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ụt

Muốn ngành cấp 2 của một mã chứng khoán thì đọc cột icb_level2client.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=2cấp — một số nguyên. icb="8300"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",
) -> DataFrame

Chứ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ểuMặc địnhNội dung
underlyingUnderlyingArgNonecơ sở: "HPG", "HPG,FPT" hoặc ["HPG", "FPT"]. None là mọi chứng quyền.
active_onlyboolTrueTrue chỉ trả mã chưa đáo hạn; False trả cả mã đã hết hạn.
refreshboolFalseBỏ qua cache.
on_errorOnError"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ộtdtypeCó thể nullĐơn vịNội dung
symbolstringkhôngMã chứng quyền. Ví dụ CHPG2525.
underlyingstringMã cơ sở. Ví dụ HPG.
issuerstringMã công ty chứng khoán phát hành. Ví dụ SSI, VND.
exercise_pricefloat64VND thôGiá thực hiện.
maturity_datedatetime64[ns]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ạn

Danh mục lớn hơn `max_rows` của gói thì bị cắt

Một lời gọi symbols()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ùng

Thu 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

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

Nội dung trang