Dữ liệu trái phiếu doanh nghiệp bằng Python
Dùng finlens lấy 6.820 lô trái phiếu doanh nghiệp Việt Nam và 940 tổ chức phát hành về pandas DataFrame, lọc theo ngành, tiền tệ, trạng thái dư nợ và khoảng đáo hạn.
Trái phiếu doanh nghiệp là phần thị trường vốn ít được số hoá nhất, và cũng là
nơi rủi ro tín dụng nằm. client.bonds trả lời hai câu: có những lô trái
phiếu nào, và ai đang nợ bao nhiêu.
6.820 lô trái phiếu của 940 tổ chức phát hành. Đây là ảnh chụp trạng thái hiện tại, không phải chuỗi thời gian dư nợ.
| Gọi | Trả lời câu hỏi |
|---|---|
client.bonds.list() | Có những lô trái phiếu nào, lãi suất bao nhiêu, đáo hạn khi nào? |
client.bonds.issuers() | Ai phát hành, dư nợ còn lại bao nhiêu, xếp hạng tín nhiệm ra sao? |
import finlens
client = finlens.client()
con_no = client.bonds.list(outstanding=True)
to_chuc = client.bonds.issuers(has_stock=True)Cột tiền đi theo CẶP, và thư viện không quy đổi
Mọi khoản tiền có hai cột: X_mvnd (triệu VND) và X_usd (USD thô). Mỗi
dòng chỉ một cột có giá trị — đúng ở cả 6.819 dòng có tiền tệ.
Không có cột tiền nào mang tên trần. df["outstanding_value"] sẽ ném
KeyError, và đó là chủ đích: một cột tiền không nói đơn vị là một cái tên nói
dối.
Vì sao không gộp hai loại tiền vào một cột
Toàn thị trường có 6.796 lô bằng VND và 23 lô bằng USD (một lô không xác định được tiền tệ). Tỷ lệ nhỏ, nên gộp lại nghe có vẻ vô hại — nhưng nó không.
Cùng một con số mệnh giá tồn tại ở cả hai loại tiền. Một cột currency đặt
cạnh không ngăn được ai đó viết df["par_value"].mean() rồi cộng đô la với đồng
lệch nhau hơn hai vạn lần.
Tách cột thì phép cộng nhầm ấy không viết ra được:
b = client.bonds.list()
b.groupby("currency")[["outstanding_value_mvnd", "outstanding_value_usd"]].sum()Hệ quả phải sống chung: câu hỏi "tổng dư nợ trái phiếu doanh nghiệp là bao nhiêu" không có một con số duy nhất ở đây. Muốn có, bạn phải tự chọn một tỷ giá — và đó là lựa chọn của bạn, không phải mặc định của thư viện. Nguồn không có bảng tỷ giá nào để thư viện dùng.
Thang đo lệch một nghìn lần so với sản phẩm MCP
Hậu tố _mvnd là triệu đồng. Sản phẩm MCP của FinLens phát cùng dữ liệu này
ở thang tỷ đồng, nên mọi cột tiền giữa hai bề mặt lệch đúng 1.000 lần.
Thang triệu đồng được chọn vì tổng giá trị phát hành của thị trường đủ lớn để chạm ngưỡng mà số thực 64 bit biểu diễn số nguyên chính xác được, nếu để đơn vị đồng thô.
list() — danh mục lô trái phiếu
client.bonds.list(
*,
codes=None,
issuer=None,
symbol=None,
sector=None,
currency=None,
status=None,
outstanding=None,
matured=None,
maturity_from=None,
maturity_to=None,
refresh=False,
on_error="warn",
) -> DataFrame| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
codes | BondCodesArg | None | Một hoặc nhiều mã trái phiếu. None là toàn bộ 6.820 lô. |
issuer | BondIssuerArg | None | Mã tổ chức phát hành. |
symbol | BondSymbolArg | None | Mã cổ phiếu của tổ chức phát hành. |
sector | BondSectorArg | None | Ngành nghề, ví dụ "Bất động sản". |
currency | BondCurrencyArg | None | "VND" (6.796) hoặc "USD" (23). |
status | BondStatusArg | None | "not_outstanding" · "fully_outstanding" · "partly_outstanding". |
outstanding | BondFlagArg | None | True gộp cả hai trạng thái còn dư nợ. |
matured | BondFlagArg | None | Lọc theo mốc đáo hạn so với thời điểm chụp. |
maturity_from · maturity_to | DateArg | None | Khoảng đáo hạn. |
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. |
Bỏ trống codes thì cả 6.820 dòng về trong một lần gọi, nên "muốn nhiều hơn
mười mã" luôn có đường đi không tốn hạn mức: lấy hết rồi lọc bằng pandas.
Cột trả về
Frame có 30 cột. Nhóm theo việc:
| Cột | dtype | Nội dung |
|---|---|---|
bond_code | string | Mã lô trái phiếu. Khoá của bảng. |
symbol | string | Mã cổ phiếu của tổ chức phát hành. Thường null — xem mục cầu nối. |
issuer · issuer_name | string | Mã và tên tổ chức phát hành. |
trading_code · isin | string | Mã giao dịch và mã ISIN. |
currency | string | VND · USD. Một dòng là null. |
business_sector | string | Ngành nghề của tổ chức phát hành. |
bond_status | string | not_outstanding (4.773) · fully_outstanding (1.392) · partly_outstanding (654). |
registration_status | string | Trạng thái đăng ký. |
coupon_rate_pct | Float64 | Lãi suất danh nghĩa, thang 0–100. |
interest_type · rate_type · interest_period | string | Kiểu lãi, kiểu điều chỉnh, kỳ trả lãi. |
principal_payment_method · interest_payment_method | string | Phương thức trả gốc và lãi. |
issue_date · maturity_date | datetime64[ns] | Ngày phát hành và ngày đáo hạn. |
issued_volume · outstanding_volume | Float64 | Khối lượng phát hành và còn lưu hành. |
par_value_mvnd · par_value_usd | Float64 | Mệnh giá. Cặp. |
issued_value_mvnd · issued_value_usd | Float64 | Giá trị phát hành. Cặp. |
outstanding_value_mvnd · outstanding_value_usd | Float64 | Dư nợ còn lại. Cặp. |
convertible · secured · green · with_warrant | boolean | Chuyển đổi · có bảo đảm · trái phiếu xanh · kèm chứng quyền. |
⚠️ coupon_rate_pct mang hậu tố _pct nên nó là thang 0–100: giá trị 9,5
nghĩa là 9,5%/năm. Khác hẳn các cột _ratio ở những trang khác.
issuers() — tổ chức phát hành
client.bonds.issuers(
*,
codes=None,
symbol=None,
sector=None,
has_bonds=None,
has_stock=None,
refresh=False,
on_error="warn",
) -> DataFrame| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
codes | BondCodesArg | None | Mã tổ chức phát hành. None là cả 940. |
symbol | BondSymbolArg | None | Lọc theo mã cổ phiếu. |
sector | BondSectorArg | None | Ngành nghề. |
has_bonds | BondFlagArg | None | Chỉ tổ chức còn trái phiếu dư nợ. |
has_stock | BondFlagArg | None | Chỉ tổ chức có mã cổ phiếu niêm yế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. |
Cột trả về
| Cột | dtype | Nội dung |
|---|---|---|
issuer_code | string | Mã tổ chức. Khoá của bảng. |
name · short_name | string | Tên đầy đủ và tên rút gọn. |
symbol | string | Mã cổ phiếu. null với 812/940 tổ chức. |
exchange | string | Sàn niêm yết của mã cổ phiếu. |
tax_code | string | Mã số thuế. |
legal_form | string | Loại hình pháp lý, giữ nguyên tiếng Việt. |
public_company_type | string | Phân loại công ty đại chúng. |
business_sector | string | Ngành nghề. |
credit_rating · rating_agency · rating_date | string · string · datetime64[ns] | Xếp hạng tín nhiệm và tổ chức xếp hạng. |
charter_capital_mvnd | Float64 | Vốn điều lệ, triệu VND. |
bond_count · outstanding_bond_count | Int64 | Đếm lô trái phiếu, không phải đếm tiền. |
outstanding_value_mvnd · outstanding_value_usd | Float64 | Tổng dư nợ. Cặp. |
first_issue_date · nearest_maturity_date | datetime64[ns] | Lần phát hành đầu và kỳ đáo hạn gần nhất. |
⚠️ legal_form không phải company_type. Bốn token CT, NH, CK, BH
của báo cáo tài chính là một từ vựng đóng khác
hẳn; legal_form ở đây là chuỗi tiếng Việt do nguồn quyết định.
Cầu nối sang cổ phiếu chỉ phủ 128/940
Cột symbol là chỗ duy nhất nối dữ liệu trái phiếu với dữ liệu giá cổ phiếu, và
nó thưa:
i = client.bonds.issuers()
i["symbol"].notna().sum() # 128
len(i) # 940Phần còn lại là doanh nghiệp chưa đại chúng hoặc chưa niêm yết. Nên khi ghép, dùng phép ghép trái và kiểm số dòng mất đi trước khi kết luận:
gia = client.eod.stock.ohlcv(
i["symbol"].dropna().tolist()[:10], start="2026-01-01"
)Nếu bạn ghép trong rồi thấy dữ liệu teo lại còn một phần bảy, đó là cầu nối thưa chứ không phải lỗi ghép.
Ảnh chụp, không phải chuỗi thời gian
Bảng này trả về trạng thái hiện tại của từng lô. Không có lịch sử dư nợ theo
kỳ, và issue_date / maturity_date là thuộc tính của lô trái phiếu chứ
không phải nhịp quan sát.
Dựng lịch đáo hạn thì nhóm trên chính ảnh chụp đó:
b = client.bonds.list(outstanding=True)
theo_thang = (
b.dropna(subset=["maturity_date"])
.groupby(b["maturity_date"].dt.to_period("M"))["outstanding_value_mvnd"]
.sum()
)Vì đây là ảnh chụp, tham số matured so với thời điểm chụp chứ không phải
thời điểm bạn gọi. Hai mốc gần nhau khi dữ liệu được thu thập đều đặn, nhưng
chúng không đồng nhất.
Một dòng không có tiền tệ
Đúng một trong 6.820 dòng có currency bằng null, và cả sáu cột tiền của
dòng đó cũng rỗng. Thư viện không lọc nó đi.
Đó là lựa chọn có chủ đích: một lô trái phiếu biến mất khỏi bảng dư nợ tệ hơn
một ô trống có thể nhìn thấy. Nếu bạn cộng theo tiền tệ thì groupby tự bỏ qua
nó; nếu bạn đếm số lô thì hãy biết nó có ở đó.
b = client.bonds.list()
b["currency"].isna().sum() # 1Câu hỏi thường gặp
Vì sao mỗi khoản tiền lại có tới hai cột thay vì một?
Vì trái phiếu trong nguồn được phát hành bằng hai loại tiền và thư viện Python finlens không quy đổi giữa chúng. Mỗi khoản tiền vì thế đi theo cặp: một cột đuôi `_mvnd` cho trái phiếu đồng Việt Nam và một cột đuôi `_usd` cho trái phiếu ngoại tệ, và trên mỗi dòng đúng một cột trong cặp có giá trị. Lý do không gộp thành một cột: cùng con số mệnh giá xuất hiện ở cả hai loại tiền, nên một cột tiền tệ đặt cạnh sẽ không ngăn được ai đó lấy trung bình cả cột và nhận một con số vô nghĩa. Đo trên dữ liệu thật, quy tắc này đúng ở cả 6.819 dòng có tiền tệ.
Đơn vị của các cột tiền trái phiếu là gì?
Cột đuôi `_mvnd` tính bằng **triệu đồng Việt Nam**, còn cột đuôi `_usd` tính bằng đô la Mỹ nguyên. Hậu tố mang cả tiền tệ lẫn thang đo nên bạn đọc được đơn vị từ chính tên cột mà không phải tra tài liệu. Thang triệu đồng được chọn vì tổng giá trị phát hành của toàn thị trường vượt quá khoảng số nguyên mà kiểu số thực biểu diễn chính xác được nếu để đơn vị đồng thô. Lưu ý sản phẩm MCP của FinLens dùng thang tỷ đồng cho cùng dữ liệu này, nên hai bề mặt lệch nhau đúng một nghìn lần.
Dữ liệu trái phiếu có phải chuỗi thời gian dư nợ theo ngày không?
Không. Đây là **ảnh chụp** tại thời điểm thu thập gần nhất, không phải chuỗi thời gian. Thư viện Python finlens trả về trạng thái hiện tại của từng lô trái phiếu — dư nợ còn lại, trạng thái đăng ký, ngày đáo hạn — chứ không trả lịch sử dư nợ qua các kỳ. Các cột ngày như `issue_date` và `maturity_date` là thuộc tính của lô trái phiếu, không phải nhịp quan sát. Nếu bạn cần dựng lịch đáo hạn theo tháng thì nhóm dữ liệu theo `maturity_date` trên chính ảnh chụp này.
Làm sao nối một lô trái phiếu với mã cổ phiếu của doanh nghiệp phát hành?
Dùng cột `symbol` có mặt trên cả hai bảng của thư viện Python finlens. Nhưng hãy biết trước giới hạn: chỉ 128 trên tổng số 940 tổ chức phát hành có mã cổ phiếu niêm yết, phần còn lại là doanh nghiệp chưa đại chúng hoặc chưa lên sàn. Vì vậy khi bạn ghép bảng trái phiếu với dữ liệu giá cổ phiếu, phần lớn tổ chức phát hành sẽ không khớp và điều đó là đúng chứ không phải lỗi ghép. Hãy dùng phép ghép trái và kiểm số dòng mất đi trước khi kết luận.
Trạng thái dư nợ của một lô trái phiếu có những giá trị nào?
Cột `bond_status` của thư viện Python finlens nhận ba giá trị: `not_outstanding` nghĩa là đã tất toán, `fully_outstanding` nghĩa là còn nguyên dư nợ, và `partly_outstanding` nghĩa là đã trả một phần. Đo trên dữ liệu thật, phân bố là 4.773 lô đã tất toán, 1.392 lô còn nguyên và 654 lô trả một phần. Muốn lọc nhanh phần còn dư nợ thì truyền tham số `outstanding` bằng đúng thay vì tự lọc theo chuỗi, vì tham số đó gộp cả hai trạng thái còn dư nợ.
Cập nhật lần cuối