Dữ liệu trong phiên (intraday) cổ phiếu Việt Nam
Lấy dữ liệu trong phiên cổ phiếu, chỉ số, phái sinh Việt Nam về pandas DataFrame: thanh giá 1min đến 4h, từng lệnh khớp, dòng tiền chủ động và basis bước 1 phút.
Bốn phương thức cho dữ liệu trong phiên: thanh giá theo bước thời gian, từng lệnh đã khớp, dòng tiền mua/bán chủ động, và chênh lệch giữa hợp đồng tương lai với chỉ số cơ sở — của phiên đang chạy hoặc một phiên đã qua.
Trước khi viết dòng đầu tiên, có hai điểm khác hẳn dữ liệu cuối ngày. Bỏ qua chúng thì code vẫn chạy, không exception nào, chỉ là kết quả sai: cột thời gian ở đây mang múi giờ, và giá ở đây là giá thô.
Bốn phương thức có ở những namespace nào
| Namespace | ohlcv() | ticks() | net_active_value() | basis() |
|---|---|---|---|---|
client.intraday.stock | ✓ | ✓ | ✓ | không có |
client.intraday.index | ✓ | ✓ | không có | không có |
client.intraday.derivative | ✓ | ✓ | ✓ | ✓ |
Ô nào ghi "không có" thì gọi vào ném AttributeError ngay tại chỗ, không phải
trả về một bảng rỗng. basis() chỉ có ở phái sinh vì nó cần một chỉ số cơ sở để
trừ, và chỉ hợp đồng tương lai mới có thứ đó.
Ba namespace tách nhau vì ba loại tài sản khác đơn vị: cổ phiếu tính giá bằng nghìn VND và khối lượng bằng cổ phiếu, chỉ số tính bằng điểm chỉ số, phái sinh tính giá bằng điểm chỉ số và khối lượng bằng hợp đồng. Một lời gọi vì vậy không bao giờ trả về bảng trộn hai đơn vị.
client.intraday.index không có net_active_value(), và đó không phải thiếu sót
Dữ liệu tick của chỉ số không ghi chiều lệnh chủ động — không có gì để tính
mua chủ động hay bán chủ động cho một chỉ số. Đây là một sự vắng mặt trong dữ
liệu, nên nó cũng là một sự vắng mặt trong API: phát ra một phương thức trả về
cột toàn null chỉ để cho đủ bộ là bắt bạn tự đoán xem đó là "không có giao
dịch" hay "không có dữ liệu".
ticks() thì vẫn có ở chỉ số, và khác biệt đó có lý do: bảng tick chỉ số vẫn
mang time, price và volume thật — chỉ hai cột side và value là null.
Bỏ luôn ticks() sẽ giấu mất bốn cột có dữ liệu thật.
df = client.intraday.index.ticks("VNINDEX", date="2026-08-03")
df["side"].isna().all() # True
df["price"].notna().any() # TrueCột time mang múi giờ, cột date của EOD thì không
Mọi bảng trong phiên đều có cột thời gian tên time, kiểu
datetime64[ns, Asia/Ho_Chi_Minh], tức mang sẵn offset +07:00. Dữ liệu cuối
ngày thì có cột date naive, không múi giờ.
Bất đối xứng này có chủ đích: một thanh trong phiên là một thời điểm, còn một phiên cuối ngày là một ngày. Một thời điểm mà không mang múi giờ thì pandas đoán là UTC, và 09:15 giờ Việt Nam bị đọc thành 16:15 — ra ngoài cả phiên giao dịch. Một ngày giao dịch thì ngược lại: gắn múi giờ vào nó là bịa ra một độ chính xác không có thật.
import finlens
client = finlens.client()
trong_phien = client.intraday.stock.ohlcv("HPG", start="2026-08-03")
trong_phien["time"].dt.tz # <DstTzInfo 'Asia/Ho_Chi_Minh' ...>
cuoi_ngay = client.eod.stock.ohlcv("HPG", start="2026-08-03")
cuoi_ngay["date"].dt.tz # NoneMúi giờ của bảng cũng nằm ở df.attrs["finlens"]["tz"]. Nếu bạn cần so với dữ
liệu nước ngoài, đổi múi giờ bằng .dt.tz_convert("UTC") — đừng gỡ múi giờ bằng
.dt.tz_localize(None), đó là cách âm thầm lệch bảy tiếng.
Mốc thời gian là đầu bước: dòng 09:15 với interval="1min" gói các lệnh
khớp từ 09:15:00 đến trước 09:16:00.
Trong phiên dán nhãn đầu bước, cuối ngày dán nhãn phiên cuối kỳ
Đừng chuyển quy ước này sang dữ liệu cuối ngày. Ở đó, một kỳ đã gộp
(interval="1mo" chẳng hạn) mang phiên giao dịch cuối cùng thực sự có dữ
liệu trong kỳ, không phải mốc đầu kỳ — 1mo của tháng 1/2025 ra 2025-01-24.
Xem Giá cuối ngày.
Hai nhóm khác nhau vì hai thứ khác nhau: một bước trong phiên là một khoảng thời gian đều đặn, còn một kỳ cuối ngày là một tập phiên giao dịch có thể thưa bất kỳ đâu vì nghỉ lễ.
Giá trong phiên là giá thô, chưa điều chỉnh quyền
client.eod.stock.ohlcv() mặc định trả giá đã điều chỉnh quyền.
client.intraday.*.ohlcv() thì luôn trả giá thô — đúng con số đã khớp trên
bảng điện hôm đó, và không có tham số adjusted ở đây.
Nối chuỗi intraday vào chuỗi EOD tạo một bậc thang giả ở mỗi lần chia tách
Hai chuỗi này không cùng cơ sở giá. Ghép thẳng chúng lại sẽ cho một chuỗi gãy đúng ở ngày chia tách hay trả cổ tức bằng cổ phiếu — và không có gì báo, vì cả hai bảng đều đúng theo cơ sở của riêng mình.
Cơ sở giá nằm ở df.attrs["finlens"]["price_basis"]; đọc nó thay vì giả định:
trong_phien.attrs["finlens"]["price_basis"] # 'raw'
cuoi_ngay.attrs["finlens"]["price_basis"] # 'adjusted'Muốn nối hai chuỗi, lấy phần EOD ở cùng cơ sở:
cuoi_ngay_tho = client.eod.stock.ohlcv("HPG", adjusted=False)
cuoi_ngay_tho.attrs["finlens"]["price_basis"] # 'raw'Chỉ số, phái sinh và chứng quyền không có khái niệm giá điều chỉnh, nên
price_basis của chúng luôn là raw ở cả hai bên.
ohlcv() — thanh giá theo bước thời gian
client.intraday.stock.ohlcv(
symbols,
*,
start=None,
end=None,
interval="1min",
refresh=False,
on_error="warn",
) -> DataFrameChỉ symbols là tham số vị trí; mọi tham số còn lại là keyword-only.
| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
symbols | SymbolsArg | — | "HPG", "HPG,VCB" hoặc ["HPG", "VCB"]. Tất cả phải cùng loại tài sản với namespace đang gọi. |
start | DateArg | None | Ngày bắt đầu. Bỏ trống thì máy chủ chọn khoảng mặc định. |
end | DateArg | None | Ngày kết thúc. Mặc định là hôm nay. |
interval | IntradayInterval | "1min" | 1min · 5min · 15min · 30min · 1h · 4h. |
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 — số phận của các mã lỗi trong lời gọi nhiều mã. |
Việc gộp nhóm chạy ở máy chủ: interval="5min" trả về thanh 5 phút đã gộp
sẵn, không phải thanh 1 phút để bạn tự gộp. Nhiều mã cũng đi trong một lời
gọi chứ không phải nhiều lời gọi tuần tự.
Độ dài khoảng thời gian được phép lấy một lần do máy chủ quyết định, đọc bằng
client.limits()["max_intraday_days"].
Interval trong phiên là một họ riêng: 1min, không phải 1m
IntradayInterval có đúng sáu giá trị: 1min, 5min, 15min, 30min, 1h,
4h. Không có 1m — trong từ vựng cũ 1m là một phút còn 1M là một
tháng, chỉ khác nhau ở chữ hoa nhưng lệch nhau 43.200 lần, nên cả hai đều bị từ
chối bằng finlens.InvalidIntervalError thay vì bị đoán ý.
Interval cuối ngày (1d, 1w, 1mo…) cũng không dùng được ở đây — muốn
thanh ngày thì gọi client.eod. Kiểu IntradayInterval được khai hẹp đúng bằng
sáu giá trị trên, nên IDE gạch đỏ ngay lúc bạn gõ, trước cả một vòng gọi mạng.
Cột trả về
| Cột | dtype | Đơn vị | null? |
|---|---|---|---|
symbol | string | — | không |
time | datetime64[ns, Asia/Ho_Chi_Minh] | — | không |
open | float64 | theo loại tài sản | có |
high | float64 | theo loại tài sản | có |
low | float64 | theo loại tài sản | có |
close | float64 | theo loại tài sản | có |
volume | float64 | theo loại tài sản | có |
Bảng luôn có đủ bảy cột đúng dtype kể cả khi không có dòng nào, nên bạn không
cần bọc if not df.empty quanh mỗi lời gọi. volume là float64 chứ không phải
int64 vì cột này nullable: một bước không có lệnh nào phải ra NaN, không phải
0.
import finlens
client = finlens.client()
df = client.intraday.stock.ohlcv("HPG", start="2026-08-03")
df.attrs["finlens"]["units"]["close"] # 'kVND'
# Thanh 5 phút, gộp ở máy chủ
df = client.intraday.stock.ohlcv(["HPG", "VCB"], interval="5min")
# Chỉ số và phái sinh nằm ở namespace riêng vì chúng khác đơn vị
vni = client.intraday.index.ohlcv("VNINDEX")
f1m = client.intraday.derivative.ohlcv("VN30F1M")Vũ trụ mã chỉ số: VNINDEX, VN30, VNXALL, HNXINDEX, HNX30, UPINDEX.
Dữ liệu của phiên đang chạy còn thay đổi
Cùng một lời gọi lúc 10:00 và lúc 15:30 cho ra hai kết quả khác nhau, và đó là
đúng chứ không phải lỗi. Mốc nước của bảng nằm ở df.attrs["finlens"]["as_of"].
ticks() — từng lệnh đã khớp
client.intraday.stock.ticks(
symbol,
*,
date=None,
refresh=False,
on_error="warn",
) -> DataFrameMỗi dòng là một lệnh đã khớp: không gộp, không resample. Muốn thanh giá theo
phút thì gọi ohlcv(); muốn dòng tiền chủ động đã cộng dồn thì gọi
net_active_value().
Một mã, một phiên cho mỗi lời gọi
Tham số đầu tên là symbol (số ít) và date là một ngày — khác hẳn hai
phương thức kia vốn nhận symbols số nhiều. Giới hạn này nằm trong chữ ký
chứ không phải trong một lỗi runtime bạn chỉ gặp sau khi request đã quá lớn.
Lý do rất cụ thể: một phiên của một mã đã là hàng chục nghìn dòng, và một phiên
phái sinh sôi động là hơn 90.000 dòng. Đây là phương thức duy nhất trả dữ
liệu chưa gộp; ohlcv() và net_active_value() nhận nhiều mã được vì chúng đọc
bản đã gộp sẵn ở máy chủ.
Truyền một danh sách vào đây sẽ nhận finlens.ValidationError trước khi chạm
mạng, chứ không âm thầm lấy mã đầu tiên. Muốn nhiều mã thì gọi nhiều lần:
khung = [client.intraday.stock.ticks(ma, date="2026-08-03")
for ma in ("HPG", "VCB")]Bỏ trống date thì máy chủ chọn phiên mặc định. Nhận cả chuỗi lẫn datetime.date:
from datetime import date
client.intraday.stock.ticks("HPG", date="2026-08-03")
client.intraday.stock.ticks("HPG", date=date(2026, 8, 3))Cột trả về
| Cột | dtype | Đơn vị | null? |
|---|---|---|---|
symbol | string | — | không |
time | datetime64[ns, Asia/Ho_Chi_Minh] | — | không |
price | float64 | giá, theo loại tài sản | có |
volume | float64 | khối lượng, theo loại tài sản | có |
side | string | — | có |
value | float64 | VND thô | có |
Một frame chỉ chứa một mã. Sáu cột luôn có mặt kể cả khi không có dòng nào.
Cột side có ba giá trị, không phải hai
| Giá trị | Nghĩa |
|---|---|
buy | Bên mua chủ động — nâng giá lên chạm bên bán. |
sell | Bên bán chủ động — hạ giá xuống chạm bên mua. |
auction | Khớp lệnh định kỳ ATO/ATC — không có bên chủ động nào. |
auction không đủ nhỏ để gộp bừa vào mua hay bán
Phiên định kỳ khớp toàn bộ lệnh tại một mức giá duy nhất, nên ở đó không tồn tại khái niệm "bên chủ động" — xếp nó vào mua hay bán đều là bịa ra một chiều không có thật. Và nó chiếm 1,4% đến 8,6% khối lượng phiên tuỳ mã, đủ lớn để làm lệch mọi con số ròng nếu bị gộp nhầm.
Muốn riêng phần khớp lệnh liên tục thì lọc ra, đừng gộp:
lien_tuc = df[df["side"] != "auction"]Với chỉ số, cả side và value đều null: dữ liệu tick chỉ số không ghi
chiều lệnh chủ động và cũng không có giá trị tiền. Hai cột vẫn có mặt với
toàn null thay vì biến mất, để một frame chỉ số vẫn concat được với một frame
cổ phiếu.
Một dòng tick mang hai họ đơn vị
Đây là chỗ dễ sai nhất của cả trang, vì nó nằm ngay trong cùng một dòng:
pricelà giá — nghìn VND với cổ phiếu, điểm chỉ số với chỉ số và phái sinh.valuelà tiền — luôn VND thô, không phụ thuộc loại tài sản.
df = client.intraday.stock.ticks("HPG", date="2026-08-03")
df.attrs["finlens"]["units"]["price"] # 'kVND'
df.attrs["finlens"]["units"]["value"] # 'VND'value do máy chủ tính, không phải phép nhân price × volume bạn tự làm
được: nó đã nhân sẵn hệ số quy đổi của loại tài sản. Nhân tay rồi so với value
mà quên đổi đơn vị sẽ lệch 1.000 lần với cổ phiếu và 100.000 lần với phái
sinh — hai con số này không có exception nào chặn được, chỉ có units chặn được.
Với phái sinh, value cũng có thể là null: quy đổi ra VND cần hệ số hợp đồng,
và máy chủ chỉ phát số cho mã đã có hệ số xác nhận. null ở đây nghĩa là
chưa biết, không phải bằng không — sai một hệ số là sai 100.000 lần, nên
null tốt hơn một con số đoán. Cột volume vẫn trả bình thường.
net_active_value() — dòng tiền mua/bán chủ động
client.intraday.stock.net_active_value(
symbols,
*,
start=None,
end=None,
interval="1min",
refresh=False,
on_error="warn",
) -> DataFrameCùng bộ tham số với ohlcv(), và cùng nhận nhiều mã trong một lời gọi. Có ở
client.intraday.stock và client.intraday.derivative; không có ở
client.intraday.index.
Giá trị và khối lượng khớp lệnh chủ động theo từng bước thời gian, tách làm ba nhóm: mua chủ động, bán chủ động, và khớp lệnh định kỳ.
Cột trả về
| Cột | dtype | Đơn vị | Nội dung |
|---|---|---|---|
symbol | string | — | Mã chứng khoán. |
time | datetime64[ns, Asia/Ho_Chi_Minh] | — | Đầu bước thời gian. |
buy_value | float64 | VND | Giá trị mua chủ động trong bước. |
sell_value | float64 | VND | Giá trị bán chủ động trong bước. |
net_value | float64 | VND | buy_value - sell_value. Dương là mua chủ động ròng. |
auction_value | float64 | VND | Giá trị khớp lệnh định kỳ ATO/ATC. |
buy_volume | float64 | khối lượng | Khối lượng mua chủ động. |
sell_volume | float64 | khối lượng | Khối lượng bán chủ động. |
net_volume | float64 | khối lượng | buy_volume - sell_volume. |
auction_volume | float64 | khối lượng | Khối lượng khớp lệnh định kỳ. |
Mười cột luôn có mặt kể cả khi không có dòng nào. Khối lượng tính bằng cổ
phiếu với cổ phiếu, bằng hợp đồng với phái sinh — đọc
df.attrs["finlens"]["units"]["buy_volume"].
Ba nhóm, hai công thức, một chỗ rất dễ hiểu nhầm
net_value = buy_value - sell_value— một hiệu.buy_value + sell_value + auction_value— một tổng, chính là toàn bộ giá trị khớp của bước đó.
Đừng cộng auction_value vào net_value. Một bên là hiệu, một bên là tổng, và
ATO/ATC không có chiều để mà cộng vào.
Ba nhóm khối lượng cộng lại đúng bằng khối lượng phiên của dữ liệu cuối ngày, nên đây là phép đối chiếu bạn tự chạy được:
trong_phien = client.intraday.stock.net_active_value("HPG", start="2026-08-03")
tong_kl = trong_phien[["buy_volume", "sell_volume", "auction_volume"]].sum().sum()
cuoi_ngay = client.eod.stock.ohlcv("HPG", start="2026-08-03", end="2026-08-03")
tong_kl == cuoi_ngay["volume"].iloc[0]KHÔNG cộng bảng này với client.eod.*.active_volume() — cùng một phiên lệch tới 520%
Nhóm cuối ngày có một phương thức tên gần giống,
client.eod.stock.active_volume() và bản phái sinh của nó.
Hai đại lượng không cộng được với nhau: cùng một phiên VN30F1M, ba cột tương
ứng lệch nhau 57,5% · 520% · 30,1%.
intraday.*.net_active_value() | eod.*.active_volume() | |
|---|---|---|
| Số rổ | ba — auction tách riêng | hai — phần định kỳ bị trộn vào |
| Cột tiền | có, tính bằng VND | không có cột tiền nào |
| Nguồn | dựng từ bảng lệnh đã khớp | một bảng tổng hợp cuối ngày |
Hệ quả trực tiếp của cột thứ nhất: buy_volume ở đây hẹp hơn
active_buy_volume ở kia, vì ở kia phiên ATO/ATC đã nằm sẵn trong một trong hai
rổ. Hai phương thức mang hai cái tên khác hẳn nhau chính vì hai định nghĩa khác
nhau — và phép cộng giữa hai frame không ném exception nào, chỉ cho ra một
con số sai.
Đơn vị là VND thô — đây là thay đổi phá vỡ so với 0.1.x
Bản 0.1.x có hai bản cài đặt khác nhau cho cùng một tên cột: bản cổ phiếu chia cho 10⁶, bản phái sinh không chia, còn tài liệu thì gọi nó là "tỷ đồng" — ba câu trả lời cho một câu hỏi.
Ở 1.x máy chủ tự tính và khai một đơn vị duy nhất: VND thô. Với cổ phiếu, con số mới lớn hơn con số cũ 10⁶ lần; với phái sinh, con số cũ vốn không thuộc đơn vị nào nên không quy đổi được — hãy đọc lại từ đầu.
df.attrs["finlens"]["units"]["net_value"] # 'VND'Với phái sinh, các cột *_value có thể trả null khi mã chưa có hệ số hợp đồng
xác nhận; các cột *_volume vẫn trả bình thường. Kiểm bằng .isna(), đừng đọc
null thành 0.
# Gộp về thanh 15 phút — giá trị được CỘNG trong bước
df = client.intraday.stock.net_active_value("HPG", interval="15min")
# Nhiều mã trong một lời gọi
df = client.intraday.stock.net_active_value(["HPG", "VCB"], interval="1h")
# Phái sinh: cùng chữ ký, khác đơn vị khối lượng
df = client.intraday.derivative.net_active_value("VN30F1M")
df.attrs["finlens"]["units"]["buy_volume"] # hợp đồng, không phải cổ phiếubasis() — chênh lệch phái sinh và chỉ số cơ sở, bước 1 phút
client.intraday.derivative.basis(
symbols,
*,
start=None,
end=None,
refresh=False,
on_error="warn",
) -> DataFrameChỉ có ở client.intraday.derivative, và chỉ phục vụ các mã VN30F*.
Cùng đại lượng và cùng công thức với
client.eod.derivative.basis(); chỉ cột thời gian khác — ở
đây là time mang múi giờ, ở kia là date naive.
Bước 1 phút cố định — không có tham số interval
Đây là phương thức duy nhất trong nhóm trong phiên không nhận interval, và
bản cuối ngày của nó cũng vậy.
Gộp một chênh lệch lên bước lớn hơn không có nghĩa hiển nhiên nào: trung bình
của bước, giá trị cuối bước và biên độ trong bước là ba câu trả lời khác nhau cho
cùng một câu hỏi. Chọn hộ một trong ba là chôn một quyết định phân tích vào thư
viện. Cần thì df.resample(...) trên chính frame này.
Cột trả về
| Cột | dtype | Đơn vị | null? |
|---|---|---|---|
symbol | string | — | không |
index_symbol | string | — | không |
time | datetime64[ns, Asia/Ho_Chi_Minh] | — | không |
future_close | float64 | điểm chỉ số | có |
spot_close | float64 | điểm chỉ số | có |
basis | float64 | điểm chỉ số | có |
basis_pct | float64 | phần trăm | có |
Bảy cột luôn có mặt kể cả khi không có dòng nào.
basis = future_close - spot_close # điểm chỉ số
basis_pct = basis / spot_close * 100 # phần trăm, thang 0–100⚠️ Mẫu số của basis_pct là spot_close, không phải future_close, và kết
quả đã nhân 100. Hai mẫu số chỉ lệch nhau khoảng 0,3% nên chọn nhầm gần
như không nhìn ra được trên một biểu đồ — đó chính là lý do công thức được viết
thẳng ra đây. Giải thích đầy đủ, kể cả vì sao index_symbol là một cột thật
chứ không phải một chi tiết trong df.attrs, nằm ở
trang cuối ngày.
df = client.intraday.derivative.basis("VN30F1M", start="2026-08-05")
df.attrs["finlens"]["units"]["basis"] # 'index_point'
df.attrs["finlens"]["units"]["basis_pct"] # 'pct'Chuỗi bắt đầu từ 09:15, và khoảng trước đó vắng mặt có chủ đích
Bar đầu tiên của một phiên là 09:15, không phải 09:00 và cũng không phải 09:01. Hai lý do rời nhau, cùng dẫn tới một mốc:
1. Trước 09:15, chỉ số chưa được tính theo từng phút. Hợp đồng phái sinh mở
cửa sớm hơn cổ phiếu nên nó có bar liên tục suốt khoảng 09:01–09:14, nhưng vế
spot_close thì không tồn tại ở đó. Đo trên 3/3 phiên kiểm: chỉ số có đúng
một bar lúc 09:00 rồi không có bar nào cho tới 09:15. Ghép hai vế ở khoảng đó
sẽ cho ra spot_close là null, tức basis cũng null — một cột toàn ô trống
suốt mười bốn phút đầu phiên.
2. Bar 09:00 có đủ hai vế, nhưng nó là một cơ chế hình thành giá khác hẳn. 09:00 là kết quả khớp lệnh định kỳ ATO: toàn bộ lệnh khớp tại một mức giá duy nhất, không có bên chủ động nào, không có chuỗi giá liên tục dẫn tới nó. Một chênh lệch tính trên hai giá đấu giá định kỳ không so sánh được với chênh lệch tính trên các bar khớp lệnh liên tục phía sau — đặt chung một chuỗi là trộn hai đại lượng.
Mốc 09:15 vì vậy loại cả hai: khoảng không có vế spot lẫn bar đấu giá định kỳ. Kết quả là một chuỗi không có ô trống nào ngay từ bar đầu.
df = client.intraday.derivative.basis("VN30F1M", start="2026-08-05")
df["time"].min().strftime("%H:%M") # '09:15'
df["basis"].isna().sum() # 0Trong phiên, bar gần nhất không phải bar thị trường vừa tạo ra
Bảng này chậm hơn thị trường một quãng có thể lên tới quãng chục phút, nên
bar cuối cùng bạn nhận được không phải phút vừa xong. Mốc nước thật của bảng nằm
ở df.attrs["finlens"]["as_of"] — so nó với đồng hồ trước khi kết luận "chênh
lệch đang là bao nhiêu ngay lúc này".
Ngoài phiên thì không có chuyện đó: một phiên đã đóng là một chuỗi đã đầy đủ.
Mã phái sinh không có chuỗi chỉ số cơ sở trong dữ liệu đi vào phần lỗi theo
từng mã với mã FL_DATA_NO_UNDERLYING (finlens.NoDataError), chứ không làm
hỏng cả lời gọi — hành vi và ví dụ giống hệt bản cuối ngày.
Đơn vị trong phiên, tra nhanh
| Loại tài sản | Cột giá (open…close, price) | Khối lượng | Cột tiền (value, *_value) |
|---|---|---|---|
| Cổ phiếu, ETF, chứng chỉ quỹ | nghìn VND — 22.3 là 22.300 đ | cổ phiếu | VND thô |
| Chỉ số | điểm chỉ số | cổ phiếu | không có |
| Phái sinh | điểm chỉ số | hợp đồng | VND thô |
Riêng basis() nằm ngoài bảng này vì cả hai vế của nó đều là điểm chỉ số
theo định nghĩa: future_close, spot_close và basis là 'index_point', còn
basis_pct là 'pct' trên thang 0–100.
Bảng trên để bạn ước lượng nhanh. Câu trả lời đáng tin duy nhất là units của
chính bảng bạn vừa nhận, tra theo tên cột:
df.attrs["finlens"]["units"]
# {'symbol': None, 'time': None, 'price': 'kVND', 'volume': 'share',
# 'side': None, 'value': 'VND'}⚠️ DataFrame.attrs không sống sót qua pd.concat hay merge của pandas.
Đọc đơn vị ra biến trước khi ghép bảng. Chi tiết ở
Kiểu dữ liệu.
Bản async
Mọi phương thức trên đề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.intraday.stock.ticks("HPG", date="2026-08-03")
asyncio.run(main())Xem thêm
Dữ liệu cuối ngày (EOD)
Giá theo phiên đã điều chỉnh quyền, sổ lệnh đặt, khối lượng chủ động, chênh lệch phái sinh và dòng tiền theo nhóm nhà đầu tư.
Kiểu dữ liệu
IntradayInterval, df.attrs["finlens"] và cách đọc đơn vị từng cột.
Xử lý lỗi
on_error, PartialFetchError và cây ngoại lệ FL_*.
FinLens MCP
Cùng dữ liệu này qua trợ lý AI, không cần viết code.
Câu hỏi thường gặp
Làm sao lấy dữ liệu theo phút của một mã cổ phiếu Việt Nam bằng Python?
Gọi `client.intraday.stock.ohlcv("HPG", start="2026-08-03", interval="5min")` của thư viện Python finlens. Chỉ `symbols` là tham số vị trí; `start`, `end`, `interval`, `refresh` và `on_error` đều keyword-only. Frame trả về gồm `symbol`, `time`, `open`, `high`, `low`, `close`, `volume` — luôn đủ bảy cột đúng dtype kể cả khi 0 dòng, nên không cần bọc `if not df.empty`. Cột thời gian tên `time` chứ không phải `date`, và nó mang múi giờ `Asia/Ho_Chi_Minh`. Việc gộp thanh chạy ở máy chủ nên `interval="5min"` trả về thanh 5 phút đã gộp sẵn.
Dữ liệu trong phiên hỗ trợ những khung thời gian nào?
Thư viện Python finlens khai kiểu `IntradayInterval` với đúng sáu giá trị: `"1min"`, `"5min"`, `"15min"`, `"30min"`, `"1h"` và `"4h"`, mặc định là `"1min"`. Không có `"1m"`: trong từ vựng cũ `1m` là một phút còn `1M` là một tháng, chỉ khác nhau ở chữ hoa nhưng lệch nhau 43.200 lần, nên cả hai đều bị từ chối bằng `finlens.InvalidIntervalError` thay vì bị đoán ý. Interval cuối ngày (`1d`, `1w`, `1mo`, `3mo`, `6mo`, `1y`) cũng không dùng được ở đây — muốn thanh ngày thì gọi `client.eod`.
Lấy từng lệnh đã khớp (tick) của một cổ phiếu bằng Python thế nào?
Gọi `client.intraday.stock.ticks("HPG", date="2026-08-03")` của thư viện Python finlens. Đây là phương thức duy nhất trong nhóm nhận `symbol` số ít và đúng một `date` cho mỗi lời gọi — giới hạn nằm trong chữ ký, nên truyền một danh sách vào sẽ nhận `finlens.ValidationError` trước khi chạm mạng. Frame gồm sáu cột `symbol`, `time`, `price`, `volume`, `side`, `value`. Cột `side` có ba giá trị: `buy` là mua chủ động, `sell` là bán chủ động và `auction` là khớp lệnh định kỳ ATO/ATC — phiên định kỳ không có bên chủ động nên đừng gộp nó vào mua hay bán.
finlens có dữ liệu trong phiên của VNINDEX và hợp đồng VN30F1M không?
Có. Chỉ số nằm ở `client.intraday.index.ohlcv("VNINDEX")` và phái sinh ở `client.intraday.derivative.ohlcv("VN30F1M")`; cả hai namespace còn có `ticks()`. Riêng `net_active_value()` chỉ có ở `client.intraday.stock` và `client.intraday.derivative` — `client.intraday.index` không có, vì dữ liệu tick của chỉ số không ghi chiều lệnh chủ động nên không có gì để tính. Ba namespace tách nhau vì khác đơn vị: cổ phiếu tính giá bằng nghìn VND, chỉ số và phái sinh tính bằng điểm chỉ số, và khối lượng phái sinh tính bằng hợp đồng.
Vì sao không nối được chuỗi giá trong phiên vào chuỗi giá cuối ngày?
Vì hai chuỗi không cùng cơ sở giá. Trong thư viện Python finlens, `client.eod.stock.ohlcv()` mặc định trả giá đã điều chỉnh quyền, còn `client.intraday.*.ohlcv()` luôn trả giá thô đúng như đã khớp trên bảng điện — và không có tham số `adjusted` ở nhóm trong phiên. Ghép thẳng hai chuỗi sẽ tạo một bậc thang giả đúng ở ngày chia tách hay trả cổ tức bằng cổ phiếu mà không có lỗi nào báo. Đọc `df.attrs["finlens"]["price_basis"]` để biết cơ sở giá, và lấy phần cuối ngày bằng `adjusted=False` nếu muốn cùng cơ sở.
Vì sao chuỗi basis trong phiên bắt đầu từ 09:15 mà không phải 09:00?
Vì hai lý do rời nhau cùng dẫn tới một mốc. Thứ nhất, trước 09:15 chỉ số chưa được tính theo từng phút: hợp đồng phái sinh mở cửa sớm hơn nên nó có bar liên tục suốt khoảng 09:01–09:14, nhưng vế `spot_close` không tồn tại ở đó, ghép vào chỉ cho ra một cột toàn ô trống. Thứ hai, bar 09:00 tuy có đủ hai vế nhưng là kết quả khớp lệnh định kỳ ATO — toàn bộ lệnh khớp tại một mức giá duy nhất, không có bên chủ động nào — nên một chênh lệch tính trên hai giá đấu giá định kỳ không so sánh được với chênh lệch tính trên các bar khớp lệnh liên tục phía sau. Trong thư viện Python finlens, `client.intraday.derivative.basis("VN30F1M")` vì vậy trả bar đầu tiên lúc 09:15 và không có ô trống nào trong cột `basis`.
Cập nhật lần cuối