Kiểu dữ liệu finlens.typing: WhoAmI, Limits, FrameMeta
Tham chiếu kiểu dữ liệu finlens.typing của thư viện Python finlens: WhoAmI, Limits, FrameMeta, EodInterval và cách đọc đơn vị từng cột trong df.attrs.
Mọi hàm truy vấn dữ liệu đều trả về pandas.DataFrame. Những gì không phải
bảng — bạn là ai, hạn mức còn bao nhiêu, bảng vừa nhận mang đơn vị gì — nằm ở
module finlens.typing.
Module này chia làm hai nhóm, phục vụ hai nhu cầu khác nhau:
- Alias kiểu — trả lời "tham số này nhận giá trị nào", và dùng để chú thích kiểu cho chính code của bạn.
TypedDict— mô tả hình dạng của các dict màwhoami(),limits()vàdf.attrs["finlens"]trả về.
Đổi tên từ 0.1.x: finlens.types → finlens.typing
Bản 0.1.x có module finlens.types với ApiKeyValidationResponse, ApiKeyMeta,
BackendMessage, MessageType và iter_messages(). Ở 1.x không tên nào trong
số đó còn tồn tại — import finlens.types ném ModuleNotFoundError.
Thay thế tương ứng: gọi client.whoami() để nhận WhoAmI, gọi client.limits()
để nhận Limits. Thông báo của máy chủ nay đi qua cơ chế warnings chuẩn của
Python, xem Xử lý lỗi.
Import ở đâu
Mọi kiểu đều được re-export ở cấp finlens, nên bạn không cần nhớ tên
module con:
import finlens
from pandas import DataFrame
def lay_gia(ma: finlens.SymbolsArg,
interval: finlens.EodInterval = "1d") -> DataFrame:
client = finlens.client()
return client.eod.stock.ohlcv(ma, interval=interval)
def con_du_han_muc(han_muc: finlens.Limits) -> bool:
return han_muc["requests_remaining"] > 100Viết from finlens.typing import Limits cũng đúng, chỉ dài hơn. Gói có gợi ý
kiểu theo chuẩn PEP 561, nên IDE và
mypy đọc được cả hai cách.
Alias kiểu
| Alias | Xuất hiện ở | Trả lời câu hỏi |
|---|---|---|
SymbolsArg | tham số symbols | Truyền một mã hay nhiều mã thế nào? |
IcbArg | tham số icb | Mã ngành viết ra sao? |
DateArg | start, end | Ngày nhận chuỗi hay date? |
EodInterval | interval | Gộp theo tuần, tháng viết là gì? |
OnError | on_error | Một mã hỏng thì cả lời gọi ra sao? |
InvestorGroup | group cấp mã và cấp chỉ số | Có những nhóm nhà đầu tư nào? |
SectorInvestorGroup | group cấp ngành | Cấp ngành có nhóm nào? |
DerivativeInvestorGroup | group cấp hợp đồng phái sinh | Phái sinh có nhóm nào? |
InvestorGroupsArg | groups cấp mã | Hỏi nhiều nhóm một lần thế nào? |
SectorInvestorGroupsArg | groups cấp ngành | Như trên, ở cấp ngành. |
DerivativeInvestorGroupsArg | groups cấp phái sinh | Như trên, ở cấp phái sinh. |
Tier | whoami()["account"]["tier"] | Gói dịch vụ tên là gì? |
SymbolsArg và IcbArg
SymbolsArg = str | Sequence[str]
IcbArg = str | Sequence[str]Ba cách viết dưới đây tương đương nhau, và hợp lệ ở mọi hàm nhận mã:
client.eod.stock.ohlcv("HPG")
client.eod.stock.ohlcv("HPG,VCB")
client.eod.stock.ohlcv(["HPG", "VCB"])IcbArg giống hệt về hình dạng nhưng khác về nội dung: nó nhận mã ngành ICB
cấp 4 dưới dạng chuỗi — "8600", "8600,3570", ["8600", "3570"]. Phải là
chuỗi chứ không phải số, vì 8600 viết dạng số sẽ làm mất số 0 đứng đầu ở các
mã khác.
DateArg
DateArg = str | date | datetime | Nonefrom datetime import date
client.eod.stock.ohlcv("HPG", start="2026-01-01")
client.eod.stock.ohlcv("HPG", start=date(2026, 1, 1))Truyền datetime cũng được, nhưng phần giờ bị bỏ: đây là ngày giao dịch,
không phải một thời điểm. None nghĩa là dùng mặc định — start mặc định là
một năm trước end, end mặc định là hôm nay.
EodInterval
EodInterval = Literal["1d", "1w", "1mo", "3mo", "6mo", "1y"]Sáu giá trị, chỉ viết thường. Không có 1m, và đó là chủ ý.
1M và 1m bị từ chối, không bị đoán ý
Trong từ vựng cũ 1M là một tháng còn 1m là một phút — phân biệt duy
nhất bằng chữ hoa, trong khi hai giá trị đó lệch nhau 43.200 lần. Ở đây tháng
là 1mo, và cả 1M lẫn 1m đều nhận một lỗi rõ ràng thay vì một biểu đồ sai:
Interval '1m' nhập nhằng: có thể là một phút hoặc một tháng.
Dùng '1min' cho một phút, hoặc '1mo' cho một tháng.Với dữ liệu cuối ngày, giá trị bạn cần là 1mo. Lỗi này là
finlens.InvalidIntervalError, mã FL_VALIDATION_INTERVAL_AMBIGUOUS.
# Đúng
client.eod.stock.ohlcv("HPG", interval="1mo")
# Sai — InvalidIntervalError
client.eod.stock.ohlcv("HPG", interval="1M")Cùng một bộ giá trị này dùng cho cả ohlcv() lẫn flow() và breakdown().
Việc gộp kỳ chạy ở phía máy chủ, nên interval="1w" trả về thanh tuần đã gộp
sẵn chứ không phải thanh ngày để bạn tự gộp.
Với năm giá trị có gộp kỳ, `date` là phiên cuối kỳ
1d trả về ngày của chính phiên đó. Năm giá trị còn lại — 1w, 1mo, 3mo,
6mo, 1y — dán nhãn mỗi dòng bằng phiên giao dịch cuối cùng thực sự có dữ
liệu trong kỳ, không phải ngày đầu kỳ và cũng không phải ngày cuối theo lịch:
df = client.eod.stock.ohlcv("HPG", start="2025-01-01", end="2025-03-31",
interval="1mo")
df["date"].dt.strftime("%Y-%m-%d").tolist()
# ['2025-01-24', '2025-02-28', '2025-03-31']Tháng 1/2025 ra 2025-01-24 vì thị trường nghỉ Tết từ ngày 25. Lý do và các hệ
quả khi lọc theo ngày ở Giá cuối ngày.
OnError
OnError = Literal["warn", "raise", "ignore"]| Giá trị | Hành vi |
|---|---|
"warn" (mặc định) | Giữ phần lấy được, phát PartialDataWarning, ghi mã hỏng vào df.attrs["finlens"]["failed"]. |
"raise" | Ném PartialFetchError, ngoại lệ này mang theo phần đã lấy được ở .data. |
"ignore" | Giữ phần lấy được, không cảnh báo. |
Mặc định là "warn" chứ không phải "raise" vì một mã hỏng không được làm mất
49 mã còn lại. Chi tiết ở Xử lý lỗi.
Nhóm nhà đầu tư
InvestorGroup = Literal[
"foreign",
"foreign_individual",
"foreign_institutional",
"local_individual",
"local_institutional",
"proprietary",
]
SectorInvestorGroup = Literal["foreign", "proprietary"]
DerivativeInvestorGroup = Literal["foreign", "proprietary"]Hai alias sau hẹp hơn có chủ đích: bốn nhóm chi tiết không tồn tại ở cấp
ngành, cũng không tồn tại ở cấp hợp đồng phái sinh. Thu hẹp Literal biến
một lỗi mạng thành một lỗi lúc bạn gõ code.
Chúng trùng tập giá trị nhưng là hai alias riêng, vì hai chỗ hẹp lại vì hai lý do khác nhau và hai lý do đó có thể đổi độc lập: ở cấp ngành, cái bị chặn là một nguồn thực chất chỉ có sàn HOSE; ở cấp phái sinh, nguồn của bốn nhóm chi tiết khoá theo danh mục doanh nghiệp niêm yết, nơi không hợp đồng phái sinh nào có mặt.
Ba alias dạng số nhiều dùng cho breakdown():
InvestorGroupsArg = str | Sequence[InvestorGroup] | None
SectorInvestorGroupsArg = str | Sequence[SectorInvestorGroup] | None
DerivativeInvestorGroupsArg = str | Sequence[DerivativeInvestorGroup] | NoneNone nghĩa là đầy đủ các nhóm mà cấp đó phục vụ, không phải "không nhóm
nào". Và không có giá trị "all" ở bất kỳ đâu — muốn tất cả thì để None,
muốn một tập cụ thể thì liệt kê ra.
Sáu nhóm không cùng một vũ trụ dữ liệu
Chỉ bốn nhóm chi tiết (foreign_individual, foreign_institutional,
local_individual, local_institutional) tạo thành một phân hoạch đầy đủ —
tổng net_value của chúng bằng 0.
foreign gồm cả giao dịch thỏa thuận nên không bằng tổng hai nhóm nước
ngoài chi tiết. proprietary (tự doanh) không là nhóm con của
local_institutional. Cộng cả sáu lại cho ra một con số vô nghĩa.
Tier
Tier = Literal["free", "standard", "pro", "premium"]Đọc bằng client.whoami()["account"]["tier"].
Các TypedDict
Đây là TypedDict, tức lúc chạy chúng là dict thuần. Truy cập bằng khóa,
không phải bằng thuộc tính, và chúng không có phương thức nào:
me = client.whoami()
me["account"]["tier"] # đúng
me.account.tier # AttributeErrorĐổi lại, bạn không phải cài thêm thư viện nào để đọc kết quả, và IDE vẫn gợi ý đủ tên khóa.
WhoAmI — client.whoami()
import finlens
client = finlens.client(api_key="flk_...")
me = client.whoami()
me["account"]["tier"] # 'pro'
me["key"]["prefix"] # 'flk_9f2a41c3'
me["limits"]["requests_remaining"] # 4812
me["version"]["latest_client_version"] # '1.1.1'
me["server_time"] # '2026-08-04T15:04:21+07:00'| Khóa | Kiểu | Nội dung |
|---|---|---|
account | AccountInfo | Gói dịch vụ và tình trạng tài khoản. |
key | KeyInfo | Khóa API đang dùng. |
version | VersionInfo | Đối chiếu phiên bản client với máy chủ. |
limits | Limits | Hạn mức hiệu lực. |
server_time | str | Giờ máy chủ, RFC 3339 kèm offset +07:00. |
Tạo client không gọi mạng; whoami() là một lời gọi thật, nên nó cũng là
cách kiểm tra khóa còn dùng được hay không ngay lúc khởi động ứng dụng.
AccountInfo
| Khóa | Kiểu | Nội dung |
|---|---|---|
tier | Tier | free · standard · pro · premium. |
status | str hoặc None | Tình trạng tài khoản. |
expires_at | str hoặc None | Thời điểm gói dịch vụ hết hạn. |
expires_at ở đây là hạn của gói dịch vụ, khác với hạn của khóa trong
KeyInfo. Hai thứ hết hạn độc lập nhau và cần hai hành động khác nhau — gia hạn
tài khoản, hay tạo khóa mới — nên chúng cũng ném hai lớp ngoại lệ khác nhau:
AccountExpiredError và ApiKeyExpiredError.
KeyInfo
| Khóa | Kiểu | Nội dung |
|---|---|---|
prefix | str | 12 ký tự đầu của khóa, ví dụ flk_9f2a41c3. |
expires_at | str hoặc None | Thời điểm khóa hết hạn. |
Khóa đầy đủ không bao giờ rời khỏi máy bạn
Máy chủ chỉ trả về 12 ký tự đầu, đủ để bạn nhận ra mình đang dùng khóa nào khi có nhiều môi trường, và không đủ để ai đó đọc log rồi dùng lại. Không có thuộc tính nào trong SDK trả ra khóa đầy đủ — nếu bạn làm mất khóa thì tạo khóa mới, không có đường lấy lại.
Bản 0.1.x làm ngược lại: nó công bố cả khóa trong phản hồi xác thực.
VersionInfo
Trùng tên với 0.1.x, khác hoàn toàn nội dung
VersionInfo là cái tên duy nhất sống sót từ finlens.types cũ sang
finlens.typing, và đó chính là chỗ dễ nhầm nhất trên trang này: autocomplete
gợi ý đúng tên, rồi code chết ở dòng sau.
Ba trường cũ client_version, backend_version, minimum_supported_version và
hai phương thức requires_upgrade(), is_out_of_support() đều không còn.
Nó nay là một TypedDict với ba khóa khác hẳn.
| Khóa | Kiểu | Nội dung |
|---|---|---|
contract_version | str | Phiên bản hợp đồng API mà máy chủ đang phục vụ. |
min_client_version | str | Bản finlens thấp nhất còn được chấp nhận. |
latest_client_version | str | Bản mới nhất trên PyPI. |
Bạn không cần tự so sánh ba con số này. Client tự làm: có bản mới hơn thì nó phát
UpgradeAvailableWarning, còn bản của bạn quá cũ để dùng thì nó ném
UpgradeRequiredError.
import finlens
client = finlens.client()
print(finlens.__version__) # bản đang chạy
print(client.whoami()["version"]["latest_client_version"])Limits — client.limits()
han_muc = client.limits()
han_muc["requests_remaining"] # 4812
han_muc["max_symbols_per_request"] # 50
han_muc["resets_at"] # '2026-08-05T00:00:00+07:00'| Khóa | Kiểu | Nội dung |
|---|---|---|
requests_per_day | int | Hạn mức lượt gọi mỗi ngày. |
requests_remaining | int | Số lượt còn lại trong chu kỳ hiện tại. |
resets_at | str | Thời điểm hạn mức được đặt lại. |
history_days | int hoặc None | Độ dài lịch sử được phép truy vấn. None là không giới hạn. |
max_symbols_per_request | int | Số mã tối đa trong một lời gọi. |
max_concurrency | int | Số request song song tối đa. |
max_rows | int | Số dòng tối đa của một kết quả. |
max_intraday_days | int | Số ngày dữ liệu trong phiên được phép lấy một lần. |
Những con số này do máy chủ quyết định và đọc được lúc chạy, không phải hằng
số biên dịch sẵn trong thư viện. Nghĩa là khi hạn mức của bạn được nới, bản
finlens đang cài sẽ dùng ngay giá trị mới mà không cần nâng cấp.
Chạm hạn mức thì bạn nhận RateLimitError hoặc DailyQuotaExceededError; sắp
cạn thì nhận QuotaLowWarning. Xem Xử lý lỗi.
Limits cũng chính là giá trị nằm ở whoami()["limits"] — client.limits()
chỉ là lối đi tắt.
FrameMeta — siêu dữ liệu đi kèm mỗi DataFrame
Đây là kiểu bạn sẽ đọc nhiều nhất. Mỗi bảng trả về đều mang theo một FrameMeta
ở df.attrs["finlens"], và nó trả lời ba câu hỏi mà bản thân bảng không trả lời
được: những con số này đơn vị gì, dữ liệu tính tới lúc nào, và có mã
nào không lấy được không.
import finlens
client = finlens.client()
df = client.eod.stock.ohlcv("HPG,VCB", start="2026-01-01")
meta = df.attrs["finlens"]
meta["dataset"] # 'eod.ohlcv'
meta["units"]["close"] # 'kVND'
meta["price_basis"] # 'adjusted'
meta["as_of"] # '2026-08-04T15:00:00+07:00'
meta["partial"] # False| Khóa | Kiểu | Nội dung |
|---|---|---|
dataset | str | Tên bộ dữ liệu, ví dụ eod.ohlcv. |
units | dict | Tên cột → đơn vị. None nghĩa là cột đó không mang đơn vị. |
price_basis | str hoặc None | adjusted (đã điều chỉnh quyền) hoặc raw. |
tz | str hoặc None | Múi giờ của các mốc thời điểm trong bảng, luôn là giờ thị trường. |
as_of | str hoặc None | Mốc nước của dữ liệu, RFC 3339 kèm offset +07:00. |
fetched_at | str | Lúc client nhận dữ liệu, theo đồng hồ máy bạn, tính bằng UTC. |
request_id | str hoặc None | Mã định danh lượt gọi. Gửi kèm khi báo lỗi. |
truncated | bool | True khi kết quả đã bị cắt ở giới hạn số dòng của gói. |
partial | bool | True khi có ít nhất một mã thất bại. |
failed | dict | Mã → lý do thất bại, mỗi giá trị là một FailedSymbol. |
Đọc đơn vị ở đâu
Đơn vị không cố định theo tên cột — nó phụ thuộc loại tài sản. Cùng cột
close: cổ phiếu là nghìn VND, chỉ số là điểm chỉ số, chứng quyền là VND thô.
Cùng cột volume: cổ phiếu tính bằng cổ phiếu, phái sinh tính bằng hợp đồng.
Vì vậy units là câu trả lời duy nhất đáng tin:
gia = client.eod.stock.ohlcv("HPG")
gia.attrs["finlens"]["units"]["close"] # 'kVND' — 23.4 nghĩa là 23.400 VND
dong_tien = client.eod.stock.investor.flow("HPG")
dong_tien.attrs["finlens"]["units"]["net_value"] # 'VND' — VND thôHai bảng trên nằm ở hai namespace khác nhau chính vì hai bộ cột đó không cùng
đơn vị. Nhân nhầm 1000 là loại lỗi không ném ngoại lệ nào — chỉ có units chặn
được nó.
Phân biệt "chưa có dữ liệu" với "dữ liệu của bạn cũ"
as_of là mốc nước do máy chủ công bố, tính theo giờ thị trường. fetched_at
là lúc máy bạn nhận bảng, tính bằng UTC. Hai trục khác nhau là có chủ đích:
meta = df.attrs["finlens"]
meta["as_of"] # '2026-08-04T15:00:00+07:00' — dữ liệu tính tới đây
meta["fetched_at"] # '2026-08-04T08:31:07+00:00' — bạn lấy về lúc nàyBảng thiếu phiên hôm nay mà as_of vẫn là hôm qua nghĩa là thị trường chưa
chốt, không phải bạn đang dùng bản cũ.
Biết một mã bị lỗi bằng cách nào
Với on_error="warn" (mặc định), lời gọi không ném lỗi — nó trả về phần lấy
được và ghi phần hỏng vào partial và failed:
df = client.eod.stock.ohlcv(["HPG", "VCB", "KHONGTONTAI"])
meta = df.attrs["finlens"]
meta["partial"] # True
list(meta["failed"]) # ['KHONGTONTAI']
meta["failed"]["KHONGTONTAI"]["code"] # 'FL_VALIDATION_SYMBOL'
meta["failed"]["KHONGTONTAI"]["message"]Kiểm partial là cách rẻ nhất để biết một lô nhiều mã có trọn vẹn hay không.
Nếu bạn muốn dừng hẳn thay vì đi tiếp, dùng on_error="raise" rồi đọc
.data và .failures của PartialFetchError.
truncated là chuyện khác: nó True khi bảng bị cắt ở giới hạn số dòng của
gói dịch vụ. Dữ liệu bạn nhận vẫn đúng, chỉ là chưa hết — thu hẹp khoảng thời
gian hoặc chia nhỏ danh sách mã rồi gọi lại.
attrs không sống sót qua merge và concat
Đây là hành vi của pandas, không phải của finlens. Ghép hai bảng bằng
pd.concat hay df.merge sẽ cho một bảng không còn attrs["finlens"] —
đơn vị, mốc nước và danh sách mã hỏng biến mất cùng lúc.
import pandas as pd
a = client.eod.stock.ohlcv("HPG")
b = client.eod.stock.ohlcv("VCB")
gop = pd.concat([a, b])
gop.attrs.get("finlens") # NoneCần giữ lại thì đọc ra biến trước khi ghép:
don_vi = a.attrs["finlens"]["units"]
gop = pd.concat([a, b])
gop.attrs["finlens"] = a.attrs["finlens"] # gán lại nếu bạn chắc hai bảng cùng đơn vịattrs mang tính thông tin, không phải cơ chế bảo vệ. Thứ bảo vệ thật là
việc tách namespace: client.eod.stock và client.eod.index không bao giờ trả
về cùng một bảng, nên một lời gọi không thể trộn nghìn VND với điểm chỉ số.
FailedSymbol
Mỗi giá trị trong failed là một dict hai khóa:
| Khóa | Kiểu | Nội dung |
|---|---|---|
code | str hoặc None | Mã lỗi ổn định, ví dụ FL_VALIDATION_SYMBOL. |
message | str | Mô tả cho người đọc. |
Phân nhánh theo code, đừng so khớp message — nội dung thông báo được phép
đổi, còn code thì không.
Hai kiểu còn lại
CacheStats là hình dạng của client.cache.stats(), gồm hits, misses,
entries, bytes, evictions. Tỷ lệ hits trên hits + misses cho biết cache
có đang giúp gì không.
client.cache.stats()["hits"]ServiceStatus là hình dạng của client.status() — trạng thái dịch vụ, đọc được
cả khi khóa API sai, nên nó là chỗ để phân biệt "backend đang có vấn đề" với
"khóa của tôi có vấn đề". Nó gồm contract_version, min_client_version,
latest_client_version, status và server_time.
Xem thêm
Dữ liệu cuối ngày (EOD)
Các hàm trả về DataFrame, tên cột và ý nghĩa từng cột.
Xử lý lỗi
Cây ngoại lệ, mã FL_* và cách bắt lỗi đúng tầng.
Câu hỏi thường gặp
Làm sao biết cột close trong DataFrame đang tính bằng đơn vị gì?
Đọc `df.attrs["finlens"]["units"]`. Mỗi DataFrame do thư viện Python finlens trả về đều mang theo một `FrameMeta` ở khoá `finlens` của `df.attrs`, trong đó `units` ánh xạ tên cột sang đơn vị: `df.attrs["finlens"]["units"]["close"]` trả `'kVND'` với cổ phiếu, nghĩa là 23.4 tương ứng 23.400 VND, còn `units["net_value"]` của bảng dòng tiền là `'VND'` thô. Đơn vị không cố định theo tên cột mà phụ thuộc loại tài sản, nên `units` là câu trả lời duy nhất đáng tin. Lưu ý `df.attrs` không sống sót qua `pd.concat` hay `df.merge`.
client.whoami() của finlens trả về những gì?
`client.whoami()` trả về một `WhoAmI` — lúc chạy là `dict` thuần — với năm khoá: `account` (`tier`, `status`, `expires_at`), `key` (`prefix` là 12 ký tự đầu của khoá, và `expires_at`), `version` (`contract_version`, `min_client_version`, `latest_client_version`), `limits`, và `server_time`. Đọc bằng khoá chứ không bằng thuộc tính: `client.whoami()["account"]["tier"]` cho ra `'pro'`. Tạo client không gọi mạng, còn `whoami()` là một lời gọi thật, nên nó cũng là cách kiểm tra khoá API còn dùng được hay không.
Kiểm tra hạn mức còn lại của khóa API bằng Python thế nào?
Gọi `client.limits()` của thư viện Python finlens. Kết quả là một `dict` kiểu `Limits` gồm `requests_per_day`, `requests_remaining`, `resets_at`, `history_days`, `max_symbols_per_request`, `max_concurrency`, `max_rows` và `max_intraday_days`, đọc bằng `client.limits()["requests_remaining"]`. Những con số này do máy chủ quyết định và đọc được lúc chạy chứ không phải hằng số biên dịch sẵn trong thư viện. Cùng giá trị đó cũng nằm ở `client.whoami()["limits"]`.
Tham số interval của finlens nhận những giá trị nào?
Có hai họ riêng biệt. `EodInterval` cho dữ liệu cuối ngày khai đúng sáu giá trị, tất cả viết thường: `1d`, `1w`, `1mo`, `3mo`, `6mo` và `1y`. `IntradayInterval` cho dữ liệu trong phiên khai sáu giá trị khác: `1min`, `5min`, `15min`, `30min`, `1h` và `4h`. Không có `1m`: trong từ vựng cũ `1M` là một tháng còn `1m` là một phút, chỉ khác nhau ở chữ hoa nhưng lệch nhau 43.200 lần, nên thư viện Python finlens từ chối thẳng cả hai bằng `finlens.InvalidIntervalError` với mã `FL_VALIDATION_INTERVAL_AMBIGUOUS` thay vì đoán ý.
Với interval là 1mo thì cột date trả về ngày nào trong tháng?
Trả về **phiên giao dịch cuối cùng thực sự có dữ liệu** trong kỳ — không phải ngày đầu kỳ, và cũng không phải ngày cuối theo lịch. Với thư viện Python finlens, `client.eod.stock.ohlcv("HPG", start="2025-01-01", end="2025-03-31", interval="1mo")` cho `2025-01-24`, `2025-02-28` rồi `2025-03-31`: tháng 1/2025 dừng ở ngày 24 vì thị trường nghỉ Tết từ ngày 25. Luật này đúng cho cả năm giá trị có gộp kỳ (`1w`, `1mo`, `3mo`, `6mo`, `1y`) và cho mọi phương thức cuối ngày; `1d` không đổi vì nó vốn đã là ngày phiên. Nhờ vậy kỳ đang dở không bao giờ mang một mốc ở tương lai.
Module finlens.types có còn trong bản 1.x không?
Không. `finlens.types` cùng `ApiKeyValidationResponse`, `ApiKeyMeta`, `BackendMessage`, `MessageType` và `iter_messages()` chỉ tồn tại ở bản 0.1.x; ở 1.x `import finlens.types` ném `ModuleNotFoundError`. Module thay thế là `finlens.typing`, và mọi kiểu đều được re-export ở cấp `finlens` nên viết `finlens.Limits` là đủ. Thông tin khoá và hạn mức nay lấy bằng `client.whoami()` và `client.limits()`. Riêng `VersionInfo` giữ nguyên tên nhưng nay là `TypedDict` ba khoá `contract_version`, `min_client_version`, `latest_client_version`, không còn phương thức nào.
Cập nhật lần cuối