Logo FinLensFinLens Docs

Xử lý lỗi finlens: FinLensError và cây ngoại lệ Python

Cách bắt lỗi khi lấy dữ liệu chứng khoán bằng finlens: except FinLensError bắt trọn cây ngoại lệ, rẽ nhánh theo mã FL_*, dùng on_error cho lời gọi nhiều mã.

Mọi lỗi thư viện finlens chủ động sinh ra đều nằm trong một cây thừa kế duy nhất, gốc là finlens.FinLensError. Trang này nói bạn nên bắt ở mức nào, đọc gì từ ngoại lệ bắt được, và phân biệt lỗi (dừng chương trình) với cảnh báo (không dừng).

Vừa bấm vào liên kết ở cuối một traceback?

error.doc_url của mọi mã lỗi đều trỏ về đúng trang này. Backend gửi một hằng số duy nhất và cố ý không ghép mã vào đuôi URL, nên FL_QUOTA_RATE không có địa chỉ riêng — tự ghép .../xu-ly-loi/FL_QUOTA_RATE hay .../xu-ly-loi#FL_QUOTA_RATE đều không dẫn tới đâu cả.

Đường ngắn nhất tới mục của bạn: mở Bảng tra mã lỗi rồi Ctrl+F (macOS: Cmd+F) dán nguyên mã vào. Bảng chia theo nhóm, mỗi nhóm là một mục trong mục lục bên phải, nên cũng có thể nhảy thẳng tới nhóm của mã — tiền tố sau FL_ chính là tên nhóm.

Một dòng bắt hết

import finlens

client = finlens.client()          # đọc FINLENS_API_KEY

try:
    df = client.eod.stock.ohlcv("HPG", start="2024-01-01", end="2024-06-30")
except finlens.FinLensError as exc:
    print(exc.code, "-", exc.message)

`except finlens.FinLensError` là đủ

Đây là cam kết của bản 1.x và có test khẳng định: mọi lớp lỗi công khai của thư viện đều là con của FinLensError.

Tài liệu của bản 0.1.x nói ngược lại — rằng except FinLensError để lọt CompanyTypeMismatchError và các lỗi bị tầng dòng tiền ném lại thành RuntimeError. Cả hai chỗ đó đã được sửa. Nếu code của bạn còn một nhánh except RuntimeError viết theo hướng dẫn cũ, nhánh đó nay là code chết — bỏ đi được.

Không cần except Exception. Bắt rộng như vậy sẽ nuốt luôn KeyboardInterrupt của bạn, lỗi gõ sai tên cột, và mọi thứ khác không liên quan đến FinLens.

Ngoại lệ mang theo những gì

Mọi lớp trong cây đều có sáu thuộc tính sau:

Thuộc tínhKiểuNội dung
.codestrMã lỗi ổn định, máy đọc được — ví dụ "FL_QUOTA_DAILY"
.messagestrMô tả cho người đọc, tiếng Việt
.request_idstr | NoneMã của request thất bại. Gửi kèm khi báo lỗi
.status_codeint | NoneMã trạng thái HTTP, nếu lỗi đến từ một response
.detailAnyDữ liệu cấu trúc kèm theo, nếu có
.doc_urlstrTrang hướng dẫn xử lý lỗi — cùng một địa chỉ cho mọi mã, không ghép mã vào đuôi

str(exc) gộp ba thứ quan trọng nhất lại:

[FL_QUOTA_DAILY] Đã dùng hết hạn mức 5000 request/ngày. Đặt lại lúc 2026-08-05T00:00:00+07:00. (request_id=req_8f3a1c) -> https://docs.finlens.vn/python-sdk/xu-ly-loi

Phần sau dấu -> là exc.doc_url, và nó là một hằng số: mọi mã lỗi đều dẫn về trang này. Đừng viết code ghép exc.code vào cuối exc.doc_url để dựng một liên kết theo mã — thứ dựng ra sẽ không tồn tại. Muốn đưa người dùng tới đúng mục thì in kèm exc.code và để họ tra trong bảng dưới.

Rẽ nhánh theo `.code`, không bao giờ theo `.message`

code là hợp đồng: nó không đổi, không được dịch. message thì được phép đổi chữ và được phép dịch bất cứ lúc nào.

try:
    df = client.eod.stock.ohlcv("HPG")
except finlens.QuotaError as exc:
    if exc.code == "FL_QUOTA_DAILY":   # đúng
        ...
    if "hạn mức" in exc.message:       # sai — sẽ hỏng khi câu chữ đổi
        ...

Một số lớp mang thêm thuộc tính riêng:

LớpThuộc tính thêm
TierError và các con.account_type (gói hiện tại), .required_tier (gói tối thiểu cần có)
RateLimitError.retry_after — số giây nên chờ
DailyQuotaExceededError.resets_at — thời điểm hạn mức đặt lại, chuỗi kèm offset +07:00
InvalidDateRangeError.max_days — số ngày tối đa server chấp nhận
PartialFetchError.data (phần lấy được) và .failures (mã → ngoại lệ)

Cây ngoại lệ

FinLensError
├── ConfigurationError            cấu hình client sai: thiếu khoá, base URL hỏng
├── UnsupportedOperationError     có trong API nhưng chưa khả dụng ở bản này
│
├── AuthError                     không xác thực được khoá
│   ├── InvalidApiKeyError        khoá không tồn tại hoặc đã bị thu hồi
│   ├── ApiKeyExpiredError        khoá hết hạn
│   ├── AccountExpiredError       gói dịch vụ hết hạn (khoá vẫn còn hiệu lực)
│   └── AccountInactiveError      tài khoản inactive hoặc banned
│
├── TierError                     gói hiện tại không cho phép thao tác này
│   ├── DatasetNotInTierError     bộ dữ liệu ngoài gói
│   └── SymbolNotInTierError      mã chứng khoán ngoài gói
│
├── QuotaError                    chạm một giới hạn sử dụng
│   ├── RateLimitError            vượt giới hạn tần suất
│   └── DailyQuotaExceededError   hết hạn mức request trong ngày
│
├── ValidationError               tham số truyền vào không hợp lệ
│   ├── InvalidSymbolError        mã sai định dạng hoặc không tồn tại
│   ├── AmbiguousFundError        mã quỹ trùng tên ở nhiều tổ chức
│   ├── InvalidDateRangeError     khoảng thời gian sai hoặc quá dài
│   ├── InvalidIntervalError      interval không hợp lệ hoặc nhập nhằng
│   ├── CompanyTypeMismatchError  các mã thuộc nhiều loại hình doanh nghiệp
│   └── UnitMismatchError         ghép các bảng khác đơn vị
│
├── TransportError                lỗi mạng, trước khi có response hợp lệ
│   ├── ConnectionFailedError     không kết nối được
│   │   └── TlsVerificationError  không xác thực được chứng chỉ TLS
│   ├── RequestTimeoutError       hết thời gian chờ
│   └── ServerError               backend trả 5xx sau khi đã thử lại
│
├── DataError                     có response nhưng nội dung không đúng hợp đồng
│   ├── DecodingError             không giải mã được payload
│   ├── SchemaMismatchError       server trả schema client không hiểu
│   ├── NoDataError               khoảng bạn hỏi không có dòng nào
│   └── PartialFetchError         một phần các mã thất bại
│
├── NotFoundError                 định danh bạn đưa vào không có ở đó
│
├── IndicatorError                TA-Lib từ chối tính một chỉ báo kỹ thuật
│
├── UpgradeRequiredError          bản client này không còn được hỗ trợ
└── IncompatibleApiVersionError   client và backend khác major của hợp đồng

AuthError, TierError, QuotaError, ValidationError, TransportError và DataError vừa là lớp cha vừa là lớp bắt được: bắt lớp cha là bắt trọn nhánh, kể cả những lớp con được thêm về sau. TlsVerificationError là ví dụ — nó là lớp con của ConnectionFailedError, nên code chỉ bắt lớp cha vẫn nhận được nó mà không phải sửa gì.

`NotFoundError` **không** phải một `NoDataError`

Hai lớp này nói hai chuyện khác nhau, và khác biệt ấy quyết định bạn phải làm gì tiếp:

  • NoDataError (FL_DATA_EMPTY) — tài nguyên có thật, chỉ là khoảng bạn hỏi không có dòng nào. Một khoảng thời gian khác có thể cứu được.
  • NotFoundError (FL_NOT_FOUND) — định danh bạn đưa vào không có ở đó. Không tham số nào cứu được; việc đúng là đi tra lại danh sách.

Vì vậy NotFoundError là con trực tiếp của FinLensError, không phải con của NoDataError. Nếu nó kế thừa, mẫu code rất hợp lý dưới đây sẽ lặng lẽ nuốt mất một cái id gõ nhầm và trả về bảng rỗng:

Ví dụ dưới đây dùng danh mục theo dõi — bề mặt duy nhất của thư viện có ghi dữ liệu, nên cũng là nơi một id gõ nhầm gây hậu quả thật.

try:
    df = client.watchlist.symbols(danh_muc_id)
except finlens.NoDataError:
    df = pd.DataFrame()              # đúng cho "danh mục chưa có mã nào"
except finlens.NotFoundError:
    raise                            # sai id — phải nổ, không được nuốt

Một 404 không kèm mã vẫn cho ra NoDataError; chỉ mã FL_NOT_FOUND, do backend phát ra tường minh, mới cho ra NotFoundError.

Nên bắt ở mức nào

Quy tắc ba dòng

Bắt lớp cha khi bạn chỉ cần biết "có lỗi". Bắt lớp lá khi mỗi loại lỗi dẫn tới một hành động khác nhau. Phân biệt bằng .code khi cùng một lớp mà hai mã cần hai cách xử lý.

Bạn đang viết gìKhuyến nghị
Notebook, script phân tích chạy một lầnMột khối except finlens.FinLensError, in exc ra rồi dừng. Đủ dùng
Job chạy định kỳ, không có người trựcTách ba nhóm: thử lại được, cần người can thiệp, lỗi trong code của bạn
Ứng dụng phục vụ người khácBắt FinLensError ở biên, hiển thị exc.message, ghi log exc.code và exc.request_id
Lời gọi nhiều mãĐừng bọc try. Dùng tham số on_error — xem mục dưới

Bảng phân loại cho nhóm job định kỳ:

NhómLớpNên làm gì
Thử lại đượcRateLimitError, RequestTimeoutError, ServerError, ConnectionFailedErrorChờ rồi chạy lại. Với RateLimitError thì chờ đúng .retry_after giây
Chờ vô íchTlsVerificationError, AuthError, TierError, UpgradeRequiredErrorBáo cho người. Nguyên nhân nằm ở máy bạn, ở tài khoản, hoặc ở phiên bản — thử lại không đổi được gì
Hết hạn mứcDailyQuotaExceededErrorChạy lại sau .resets_at, không sớm hơn
Lỗi trong code của bạnValidationError và các conSửa code. Chạy lại y nguyên sẽ ra đúng lỗi đó

Hai dòng đầu giao nhau ở một chỗ: TlsVerificationError là một ConnectionFailedError. Nếu bạn bắt lớp cha để thử lại thì phải đặt nhánh TlsVerificationError lên trước, không thì một lỗi vĩnh viễn sẽ bị thử lại mãi mà không bao giờ khác đi.

import time
import finlens

client = finlens.client()

try:
    df = client.eod.stock.investor.flow("HPG", start="2024-01-01")
except finlens.RateLimitError as exc:
    time.sleep(exc.retry_after or 60)
except finlens.DailyQuotaExceededError as exc:
    print("Hạn mức đặt lại lúc", exc.resets_at)
except finlens.AuthError as exc:
    print("Kiểm tra lại khoá API:", exc.message)
except finlens.FinLensError as exc:
    print(exc.code, exc.message, exc.request_id)

Thứ tự except đi từ hẹp tới rộng. Đặt FinLensError lên trên cùng thì mọi nhánh phía dưới không bao giờ chạy.

ValidationError vẫn là ValueError

ValidationError kế thừa cả FinLensError và ValueError, và đó là chủ đích: code cũ viết except ValueError quanh lời gọi FinLens vẫn tiếp tục chạy sau khi nâng lên 1.x.

try:
    df = client.eod.stock.ohlcv("HPG", interval="1M")
except ValueError as exc:          # vẫn bắt được
    print(exc)

Nhưng khi viết mới thì bắt finlens.ValidationError: nó chỉ bắt lỗi tham số của FinLens, không nuốt luôn ValueError từ code khác của bạn.

Ba lỗi tham số hay gặp nhất:

  • InvalidIntervalError — interval chỉ nhận 1d, 1w, 1mo, 3mo, 6mo, 1y. 1M và 1m bị từ chối thẳng thay vì đoán ý: trong từ vựng cũ hai token đó chỉ khác nhau ở chữ hoa nhưng lệch nhau 43.200 lần.
  • InvalidDateRangeError — khoảng thời gian vượt giới hạn. Giới hạn thật do server quyết định, đọc bằng client.limits(), không phải hằng số trong thư viện.
  • CompanyTypeMismatchError — các mã thuộc nhiều loại hình doanh nghiệp khác nhau (phi tài chính, ngân hàng, chứng khoán, bảo hiểm). Cây chỉ tiêu báo cáo tài chính của chúng khác nhau nên không gộp vào một bảng được.

Nhiều mã trong một lời gọi: on_error

Khi bạn xin 50 mã và 1 mã hỏng, 49 mã còn lại không được mất theo. Đó là việc của tham số on_error, có ở mọi hàm nhận nhiều mã.

Giá trịHành vi
"warn" (mặc định)Giữ phần thành công, phát PartialDataWarning. Chương trình chạy tiếp
"raise"Ném PartialFetchError — mang theo phần đã lấy được ở .data
"ignore"Giữ phần thành công, không nói gì cả

Cả ba trường hợp đều ghi lại chi tiết vào df.attrs["finlens"]:

df = client.eod.stock.ohlcv(ma_list, start="2024-01-01")   # on_error="warn"

if df.attrs["finlens"]["partial"]:
    for symbol, ly_do in df.attrs["finlens"]["failed"].items():
        print(symbol, ly_do["code"], ly_do["message"])

Với on_error="raise", dữ liệu đi kèm ngoại lệ nên bạn không phải gọi lại từ đầu:

try:
    df = client.eod.stock.ohlcv(ma_list, on_error="raise")
except finlens.PartialFetchError as exc:
    df = exc.data                    # 49 mã thành công, vẫn dùng được
    print(exc.failures)              # {"XYZ": InvalidSymbolError(...)}

exc.failures ánh xạ mã chứng khoán → ngoại lệ tương ứng, nên mỗi mã hỏng vẫn giữ đủ .code và .message của riêng nó.

`df.attrs` không sống sót qua `merge` và `concat`

pandas không chuyển attrs qua các phép ghép bảng. Đọc partial, failed, units trước khi ghép, hoặc lưu lại giá trị bạn cần. Đây là thông tin đi kèm dữ liệu, không phải một cơ chế bảo vệ.

Kết quả không có dòng nào không phải lỗi. Một khoảng ngày nghỉ, hay một mã mới niêm yết, trả về DataFrame rỗng với đúng cột và đúng dtype — không cảnh báo, không ngoại lệ. NoDataError chỉ dành cho trường hợp server báo rõ bằng mã FL_DATA_EMPTY rằng nó không có dữ liệu cho tài nguyên này. Còn khi chính định danh bạn đưa vào không tồn tại thì đó là NotFoundError (FL_NOT_FOUND), một lớp khác hẳn.

TlsVerificationError: máy bạn nằm sau proxy kiểm tra TLS

Rất phổ biến trong mạng doanh nghiệp: proxy kiểm tra TLS hoặc phần mềm diệt virus chèn vào giữa kết nối bằng một CA riêng, mà bundle chứng chỉ mặc định của Python không có CA đó.

Đây là lỗi vĩnh viễn, khác hẳn một lỗi kết nối thường: thử lại không bao giờ cứu được, và cách sửa nằm ở máy bạn chứ không ở phía FinLens. Vì vậy thư viện không thử lại nó.

client = finlens.client(ca_bundle="/duong/dan/toi/ca-bundle.pem")

Hoặc đặt biến môi trường một lần cho cả máy:

# Linux / macOS
export FINLENS_CA_BUNDLE="/duong/dan/toi/ca-bundle.pem"

# Windows PowerShell
$env:FINLENS_CA_BUNDLE = "C:\certs\ca-bundle.pem"

File CA bundle do bộ phận IT của bạn cấp — đó là chứng chỉ gốc của chính proxy đang đứng giữa.

Khác với 0.1.x

Ở bản 0.1.x, lỗi chứng chỉ bị báo cáo thành "backend đang không phản hồi", gửi người đọc đi kiểm đúng chỗ không có vấn đề gì. Nếu bạn đang gặp thông báo đó trong khi curl cùng lúc vẫn chạy, hãy nâng cấp bằng pip install -U finlens rồi đọc lại thông báo mới.

TlsVerificationError kế thừa ConnectionFailedError, nên code đang bắt lớp cha không phải sửa gì.

SchemaMismatchError: response sai hình dạng

Ném ra khi server trả về một cấu trúc mà phiên bản client này không hiểu. Hai nguyên nhân, theo thứ tự khả năng:

  1. FINLENS_BASE_URL đang trỏ nhầm chỗ — thường là một endpoint cũ hoặc một backend cục bộ đang chạy phiên bản khác. Kiểm biến môi trường trước tiên.
  2. Client quá cũ so với backend. Nâng cấp bằng pip install -U finlens.

Đây là thay đổi hành vi so với 0.1.x

Ở bản 0.1.x, một response sai hình dạng trả về bảng rỗng trong im lặng. Người dùng đọc "khối ngoại mua ròng 0 đồng" trong khi thật ra client không hiểu được response — không ngoại lệ, không cảnh báo.

Nay nó ném lỗi. Một traceback to rõ ràng tốt hơn một con số sai trông rất thật.

IndicatorError và bốn mã FL_TA*

Chỉ báo kỹ thuật chạy hoàn toàn trên máy bạn và không đụng tới một response nào, nên nó không nằm dưới DataError — vốn được định nghĩa là "nhận được response nhưng nội dung không đúng hợp đồng". IndicatorError vì vậy là con trực tiếp của FinLensError.

Bốn mã chia làm hai nhóm theo ai sửa được:

MãLớpAi sửa
FL_TA_PARAMValidationErrorBạn — một tham số sai kiểu hoặc sai giá trị
FL_TA_INPUTValidationErrorBạn — frame thiếu cột, hoặc các mảng lệch độ dài
FL_TAIndicatorErrorBạn — TA-Lib từ chối tổ hợp tham số này
FL_TA_UNAVAILABLEIndicatorErrorMáy bạn — phần native của TA-Lib không nạp được

Hai mã đầu là ValidationError, nên chúng cũng là ValueError và except ValueError sẵn có của bạn vẫn bắt được:

finlens.ta.RSI(close, timeperiod=14.5)
# ValidationError: [FL_TA_PARAM] `RSI`: tham số `timeperiod` phải là số nguyên,
# nhận 14.5. TA-Lib cắt phần thập phân trong im lặng, nên `timeperiod=14.5` sẽ
# chạy như `timeperiod=14` mà không báo gì.

df[["symbol", "date", "close"]].finlens.atr(14)
# ValidationError: [FL_TA_INPUT] `ATR` cần cột `high` nhưng frame không có.
# Frame này là `eod.ohlcv`. Các cột đang có: symbol, date, close.

finlens.ta.RSI(close, timeperiod=1)
# IndicatorError: [FL_TA] `RSI` lỗi bên trong TA-Lib: TA_RSI function failed
# with error code 2: Bad Parameter (TA_BAD_PARAM)

Việc bọc là cần thiết chứ không phải trang trí: TA-Lib ném Exception trần, nên nếu để nguyên thì thứ duy nhất bắt được nó là except Exception — và cam kết "except finlens.FinLensError là đủ" sẽ không còn đúng. Lỗi gốc luôn được giữ ở __cause__.

Một `TypeError` không phải lỗi của FinLens vẫn có thể tới từ chỉ báo

Tham số của finlens.ta.* là keyword-only, nên finlens.ta.RSI(close, 14) ném một TypeError của Python — không phải FinLensError, và except finlens.FinLensError không bắt được nó.

Đó là chủ đích: đây là lỗi cú pháp lời gọi, cùng loại với gõ sai tên tham số, và nó phải chết ngay lúc bạn chạy dòng đầu tiên chứ không đi vào một nhánh xử lý lỗi dữ liệu.

Bảng tra mã lỗi

Ánh xạ là nhiều-đối-một: nhiều mã có thể trỏ về cùng một lớp. Bắt theo lớp, phân biệt theo mã.

Bảng chia theo nhóm — tiền tố ngay sau FL_ chính là tên nhóm, nên FL_QUOTA_RATE nằm ở Hạn mức và FL_TRANSPORT_TLS nằm ở Đường truyền và server. Mỗi nhóm là một mục trong mục lục bên phải. Dấu ... là chỗ thông báo thật điền giá trị cụ thể của lời gọi.

Cấu hình

MãLớp ngoại lệThông báo bạn thấy
FL_CONFIGConfigurationErrorCấu hình client không hợp lệ: ...

Xác thực

MãLớp ngoại lệThông báo bạn thấy
FL_AUTHAuthErrorMã nhóm cho một lỗi 401 không kèm mã cụ thể hơn. Thông báo do endpoint viết ra
FL_AUTH_INVALID_KEYInvalidApiKeyErrorAPI key không hợp lệ hoặc đã bị thu hồi
FL_AUTH_KEY_EXPIREDApiKeyExpiredErrorAPI key đã hết hạn vào ...
FL_AUTH_ACCOUNT_EXPIREDAccountExpiredErrorGói FinLens của bạn đã hết hạn vào ... Vui lòng gia hạn để tiếp tục
FL_AUTH_ACCOUNT_INACTIVEAccountInactiveErrorTài khoản đang ở trạng thái ...

Gói dịch vụ

MãLớp ngoại lệThông báo bạn thấy
FL_TIERTierErrorMã nhóm cho một lỗi 403 không kèm mã cụ thể hơn. Thông báo do endpoint viết ra
FL_TIER_DATASETDatasetNotInTierErrorBộ dữ liệu ... cần gói ... (bạn đang dùng ...)
FL_TIER_SYMBOLSymbolNotInTierErrorMã ... cần gói ... (bạn đang dùng ...)

Hạn mức

MãLớp ngoại lệThông báo bạn thấy
FL_QUOTAQuotaErrorMã nhóm cho một lỗi 429 không kèm mã cụ thể hơn. Thông báo do endpoint viết ra
FL_QUOTA_RATERateLimitErrorVượt giới hạn tần suất. Thử lại sau ... giây
FL_QUOTA_DAILYDailyQuotaExceededErrorĐã dùng hết hạn mức ... request/ngày. Đặt lại lúc ...

Tham số đầu vào

MãLớp ngoại lệThông báo bạn thấy
FL_VALIDATIONValidationErrorTham số request không hợp lệ. Với 422 của FastAPI, chi tiết từng trường nằm ở .detail["errors"]
FL_VALIDATION_SYMBOLInvalidSymbolErrorMã ... không tồn tại
FL_VALIDATION_FUND_AMBIGUOUSAmbiguousFundErrorMã quỹ ... thuộc ... tổ chức. Gọi lại bằng organization_id: ...
FL_VALIDATION_DATE_RANGEInvalidDateRangeErrorKhoảng thời gian quá lớn: yêu cầu ... ngày, tối đa ...
FL_VALIDATION_INTERVALInvalidIntervalErrorInterval ... không hợp lệ. Giá trị hợp lệ: ...
FL_VALIDATION_INTERVAL_AMBIGUOUSInvalidIntervalErrorInterval ... nhập nhằng: có thể là một phút hoặc một tháng
FL_VALIDATION_CURSORValidationErrorTham số cursor không đọc được. Hãy truyền lại đúng nguyên văn meta.next_cursor của response trước
FL_VALIDATION_COMPANY_TYPECompanyTypeMismatchErrorCác mã thuộc nhiều loại hình doanh nghiệp ...
FL_VALIDATION_KIND_NOT_AVAILABLEValidationErrorLoại báo cáo ... không tồn tại với doanh nghiệp loại ... Các loại báo cáo còn dùng được: ...
FL_VALIDATION_UNIT_MISMATCHUnitMismatchErrorKhông thể ghép các bảng khác đơn vị: ... và ...

FL_VALIDATION_FUND_AMBIGUOUS là mã duy nhất mà lối thoát nằm trong .detail: detail["organization_ids"] chứa các organization_id bạn phải chọn một, và không có cách nào khác tự tìm ra chúng.

Dữ liệu và tài nguyên

MãLớp ngoại lệThông báo bạn thấy
FL_DATADataErrorKhông tìm được phiên giao dịch nào trong ... ngày quanh ... Lịch giao dịch đang dùng có vẻ hỏng
FL_DATA_EMPTYNoDataErrorKhông có dữ liệu cho ...
FL_DATA_NO_UNDERLYINGNoDataErrorMã ... không có chuỗi chỉ số cơ sở trong dữ liệu
FL_DATA_SCHEMASchemaMismatchErrorServer trả về schema mà phiên bản client này không hiểu
FL_DATA_DECODEDecodingErrorServer trả về dữ liệu không phải JSON hợp lệ. Kiểm tra xem có proxy nào chèn giữa không
FL_DATA_PARTIALPartialFetchError.../... mã thất bại
FL_NOT_FOUNDNotFoundErrorKhông tìm thấy tài nguyên với định danh này

FL_DATA_EMPTY và FL_DATA_NO_UNDERLYING cùng cho NoDataError — bắt lớp là bắt được cả hai — nhưng chúng là hai bệnh khác nhau: mã đầu nói khoảng bạn hỏi không có dòng nào, mã sau nói hợp đồng này không có chuỗi chỉ số cơ sở trong dữ liệu, tức không đổi khoảng nào cứu được. FL_NOT_FOUND thì cho một lớp khác hẳn — xem cây ngoại lệ.

Chỉ báo kỹ thuật

MãLớp ngoại lệThông báo bạn thấy
FL_TA_PARAMValidationError...: tham số ...=... không hợp lệ. ...
FL_TA_INPUTValidationError...: dữ liệu đầu vào không dùng được. ...
FL_TAIndicatorError... lỗi bên trong TA-Lib: ...
FL_TA_UNAVAILABLEIndicatorErrorKhông nạp được TA-Lib: ... Cài lại bằng: pip install --force-reinstall ta-lib

Đường truyền và server

MãLớp ngoại lệThông báo bạn thấy
FL_TRANSPORT_CONNECTConnectionFailedErrorKhông kết nối được tới ...
FL_TRANSPORT_TLSTlsVerificationErrorXác thực chứng chỉ TLS thất bại khi nối tới ...
FL_TRANSPORT_TIMEOUTRequestTimeoutErrorRequest hết thời gian chờ sau ... giây
FL_TRANSPORT_SERVERServerErrorBackend đang không phản hồi (circuit breaker đang mở). Client sẽ tự thăm dò lại sau vài giây
FL_SERVERServerErrorLỗi phía server. request_id=...
FL_SERVICE_UNAVAILABLEServerErrorDịch vụ tạm thời không sẵn sàng. Vui lòng thử lại sau giây lát

Ba mã cuối cùng cho ra ServerError nhưng nói ba chuyện khác nhau, và bạn nên đọc .code để biết có nên thử lại không. FL_SERVICE_UNAVAILABLE là 503 kèm Retry-After: hạ tầng tạm thời không sẵn sàng, thử lại là đúng. FL_TRANSPORT_SERVER là do chính client dựng lên khi circuit breaker mở — nó chưa gửi request nào đi cả. FL_SERVER là 500, một lỗi không xác định.

Phiên bản

MãLớp ngoại lệThông báo bạn thấy
FL_VERSION_UPGRADE_REQUIREDUpgradeRequiredErrorPhiên bản client này không còn được hỗ trợ. Tối thiểu là ...
FL_VERSION_API_INCOMPATIBLEIncompatibleApiVersionErrorClient dùng contract major ..., server dùng ...

Mã không có trong bảng

Một mã không có trong bảng này vẫn được ánh xạ về một lớp trong cây theo HTTP status của response — không bao giờ thành một RuntimeError trần. Đó là lý do except finlens.FinLensError giữ được lời hứa của mình kể cả khi backend thêm mã mới.

HTTP statusLớp ngoại lệ
400 · 409 · 422ValidationError
401AuthError
403TierError
404NoDataError
426UpgradeRequiredError
429QuotaError
500 · 502 · 503 · 504ServerError

Cảnh báo khác lỗi

Cảnh báo không dừng chương trình. Chúng đi qua module warnings của Python, nên bạn tắt hay bật từng loại một cách chọn lọc.

FinLensWarning (UserWarning)
├── UpgradeAvailableWarning   có bản client mới hơn trên PyPI
├── QuotaLowWarning           hạn mức sắp cạn. Chỉ phát một lần mỗi client
├── PartialDataWarning        một phần các mã thất bại, với on_error="warn"
├── DataQualityWarning        dữ liệu trả về có vấn đề đã biết
└── TierLimitWarning          yêu cầu đã bị cắt bớt cho vừa gói dịch vụ

FinLensDeprecationWarning (DeprecationWarning)
    một phần của API công khai sắp bị gỡ bỏ

Tắt một loại, giữ nguyên các loại còn lại:

import warnings
import finlens

warnings.filterwarnings("ignore", category=finlens.QuotaLowWarning)

Hoặc làm ngược lại — biến cảnh báo thành lỗi để nó không thể bị bỏ qua. Hữu ích trong job chạy tự động, nơi không ai đọc stderr:

warnings.filterwarnings("error", category=finlens.PartialDataWarning)

Lúc đó PartialDataWarning sẽ được ném ra như một ngoại lệ và dừng chương trình — khác với on_error="raise" ở chỗ bạn không nhận được .data.

Tắt toàn bộ cảnh báo của FinLens mà không đụng tới cảnh báo của thư viện khác:

warnings.filterwarnings("ignore", category=finlens.FinLensWarning)

Thư viện không còn tắt cảnh báo của bạn

Bản 0.1.x gọi warnings.filterwarnings("ignore") ngay lúc import, tức là tắt mọi cảnh báo trong toàn bộ tiến trình của bạn — kể cả cảnh báo của pandas, numpy và code của chính bạn. Bản 1.x không làm vậy nữa.

FinLensDeprecationWarning kế thừa DeprecationWarning chứ không kế thừa FinLensWarning, theo đúng quy ước Python: mặc định ẩn với người dùng cuối, hiện ra khi chạy python -W default.

Khi cần báo lỗi

Gửi kèm ba thứ, chúng cắt ngắn phần lớn cuộc trao đổi qua lại:

import finlens

print(finlens.__version__)
print(finlens.build_info())     # commit và thời điểm build của binary bạn đang chạy

Cộng thêm exc.request_id của lời gọi hỏng — đó là thứ cho phép tra lại đúng request đó ở phía FinLens.

Danh sách thay đổi từng phiên bản: Nhật ký thay đổi.

Câu hỏi thường gặp

Bắt lỗi của thư viện finlens bằng cách nào?

Dùng `except finlens.FinLensError`. Ở bản 1.x mọi lớp lỗi công khai của thư viện Python finlens đều kế thừa `FinLensError`, nên một khối `except` là đủ và không cần `except Exception`. Ngoại lệ bắt được mang `.code` (mã ổn định như `FL_QUOTA_DAILY`), `.message` bằng tiếng Việt, `.request_id`, `.status_code`, `.detail` và `.doc_url`; `str(exc)` gộp sẵn ba thứ quan trọng nhất. Luôn rẽ nhánh theo `.code`, đừng so khớp `.message` — nội dung thông báo được phép đổi chữ, còn `code` thì không.

Vượt hạn mức khi gọi API dữ liệu chứng khoán bằng Python thì bắt lỗi nào?

Với thư viện Python finlens, cả hai trường hợp đều nằm dưới `finlens.QuotaError`. Vượt giới hạn tần suất là `RateLimitError` (mã `FL_QUOTA_RATE`), mang thêm `.retry_after` là số giây nên chờ. Hết hạn mức trong ngày là `DailyQuotaExceededError` (mã `FL_QUOTA_DAILY`), mang `.resets_at` là thời điểm hạn mức được đặt lại kèm offset `+07:00` — chạy lại sau mốc đó, không sớm hơn. Trước khi cạn, thư viện phát `QuotaLowWarning` một lần cho mỗi client.

Một mã lỗi FL_ có tương ứng một lớp ngoại lệ riêng không?

Không, ánh xạ là nhiều-đối-một: nhiều mã có thể trỏ về cùng một lớp ngoại lệ của thư viện Python finlens. Ví dụ `FL_VALIDATION_INTERVAL` và `FL_VALIDATION_INTERVAL_AMBIGUOUS` cùng cho `InvalidIntervalError`, còn `FL_DATA_EMPTY` và `FL_DATA_NO_UNDERLYING` cùng cho `NoDataError`. Vì vậy nên bắt theo lớp rồi phân biệt theo `.code`. Một mã không có trong bảng tra vẫn được ánh xạ về một lớp trong cây theo HTTP status của response (401 sang `AuthError`, 403 sang `TierError`, 404 sang `NoDataError`, 429 sang `QuotaError`, 5xx sang `ServerError`), không bao giờ thành `RuntimeError` trần — đó là lý do `except finlens.FinLensError` vẫn đúng khi máy chủ thêm mã mới.

Gọi nhiều mã cùng lúc mà một mã hỏng thì chuyện gì xảy ra?

Tham số `on_error` quyết định, và mặc định là `"warn"`: thư viện Python finlens giữ phần lấy được, phát `PartialDataWarning`, rồi ghi chi tiết vào `df.attrs["finlens"]["partial"]` và `df.attrs["finlens"]["failed"]` (mã chứng khoán ánh xạ sang `code` và `message`). Đặt `on_error="raise"` thì nhận `PartialFetchError`, ngoại lệ này mang theo phần đã lấy được ở `.data` và ánh xạ mã sang ngoại lệ ở `.failures`, nên không phải gọi lại từ đầu. `on_error="ignore"` giữ dữ liệu và không cảnh báo gì.

Lỗi xác thực chứng chỉ TLS khi kết nối tới FinLens sửa thế nào?

Đó là `finlens.TlsVerificationError`, mã `FL_TRANSPORT_TLS`, thường gặp khi máy bạn nằm sau proxy kiểm tra TLS hoặc phần mềm diệt virus dùng CA riêng. Cách sửa là trỏ thư viện tới CA bundle do bộ phận IT cấp: `finlens.client(ca_bundle="/duong/dan/ca-bundle.pem")`, hoặc đặt biến môi trường `FINLENS_CA_BUNDLE`. Đây là lỗi vĩnh viễn nên thư viện không thử lại và nó cũng không làm mở circuit breaker. Lớp này kế thừa `ConnectionFailedError`, nên code đang bắt lớp cha không phải sửa gì.

Liên kết ở cuối thông báo lỗi của finlens dẫn đi đâu?

Dẫn về đúng trang xử lý lỗi của thư viện Python finlens: `https://docs.finlens.vn/python-sdk/xu-ly-loi`. Đó là một hằng số — mọi mã lỗi `FL_*` đều dùng chung một địa chỉ, backend cố ý không ghép `code` vào đuôi URL và trang cũng không có neo riêng cho từng mã. Vì vậy đừng dựng liên kết bằng cách nối `exc.code` vào `exc.doc_url`; địa chỉ dựng ra sẽ không tồn tại. Cách tra nhanh mã của bạn là mở trang đó, tới mục Bảng tra mã lỗi rồi tìm nguyên văn mã — bảng chia theo nhóm, và tiền tố ngay sau `FL_` chính là tên nhóm.

Code cũ viết except ValueError quanh lời gọi finlens có còn chạy không?

Còn. `finlens.ValidationError` kế thừa cả `FinLensError` lẫn `ValueError`, và đó là chủ đích để code viết cho bản cũ không gãy khi nâng lên 1.x: các lỗi tham số như `InvalidSymbolError`, `InvalidDateRangeError`, `InvalidIntervalError`, `CompanyTypeMismatchError` và `UnitMismatchError` vẫn lọt vào `except ValueError`. Khi viết mới thì nên bắt `finlens.ValidationError` — nó chỉ bắt lỗi tham số của finlens chứ không nuốt luôn `ValueError` từ code khác của bạn.

Cập nhật lần cuối

Nội dung trang