Logo FinLensFinLens Docs

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

Namespaceohlcv()ticks()net_active_value()basis()
client.intraday.stockkhông có
client.intraday.indexkhô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, pricevolume thật — chỉ hai cột sidevaluenull. 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()    # True

Cộ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              # None

Mú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",
) -> DataFrame

Chỉ symbols là tham số vị trí; mọi tham số còn lại là keyword-only.

Tham sốKiểuMặc địnhNội dung
symbolsSymbolsArg"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.
startDateArgNoneNgày bắt đầu. Bỏ trống thì máy chủ chọn khoảng mặc định.
endDateArgNoneNgày kết thúc. Mặc định là hôm nay.
intervalIntradayInterval"1min"1min · 5min · 15min · 30min · 1h · 4h.
refreshboolFalseBỏ qua bản trong cache và lấy lại từ máy chủ.
on_errorOnError"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ộtdtypeĐơn vịnull?
symbolstringkhông
timedatetime64[ns, Asia/Ho_Chi_Minh]không
openfloat64theo loại tài sản
highfloat64theo loại tài sản
lowfloat64theo loại tài sản
closefloat64theo loại tài sản
volumefloat64theo loại tài sản

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. volumefloat64 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",
) -> DataFrame

Mỗ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à datemộ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()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ộtdtypeĐơn vịnull?
symbolstringkhông
timedatetime64[ns, Asia/Ho_Chi_Minh]không
pricefloat64giá, theo loại tài sản
volumefloat64khối lượng, theo loại tài sản
sidestring
valuefloat64VND thô

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
buyBên mua chủ động — nâng giá lên chạm bên bán.
sellBên bán chủ động — hạ giá xuống chạm bên mua.
auctionKhớ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ả sidevalue đề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:

  • pricegiá — nghìn VND với cổ phiếu, điểm chỉ số với chỉ số và phái sinh.
  • valuetiề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ếu100.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",
) -> DataFrame

Cù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.stockclient.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ộtdtypeĐơn vịNội dung
symbolstringMã chứng khoán.
timedatetime64[ns, Asia/Ho_Chi_Minh]Đầu bước thời gian.
buy_valuefloat64VNDGiá trị mua chủ động trong bước.
sell_valuefloat64VNDGiá trị bán chủ động trong bước.
net_valuefloat64VNDbuy_value - sell_value. Dương là mua chủ động ròng.
auction_valuefloat64VNDGiá trị khớp lệnh định kỳ ATO/ATC.
buy_volumefloat64khối lượngKhối lượng mua chủ động.
sell_volumefloat64khối lượngKhối lượng bán chủ động.
net_volumefloat64khối lượngbuy_volume - sell_volume.
auction_volumefloat64khối lượngKhố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ổbaauction tách riênghai — phần định kỳ bị trộn vào
Cột tiềncó, tính bằng VNDkhông có cột tiền nào
Nguồndựng từ bảng lệnh đã khớpmộ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ếu

basis() — 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",
) -> DataFrame

Chỉ 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ộtdtypeĐơn vịnull?
symbolstringkhông
index_symbolstringkhông
timedatetime64[ns, Asia/Ho_Chi_Minh]không
future_closefloat64điểm chỉ số
spot_closefloat64điểm chỉ số
basisfloat64điểm chỉ số
basis_pctfloat64phần trăm

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_pctspot_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ó 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_closenull, 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()             # 0

Trong 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ảnCột giá (openclose, price)Khối lượngCột tiền (value, *_value)
Cổ phiếu, ETF, chứng chỉ quỹnghìn VND22.3 là 22.300 đcổ phiếuVND thô
Chỉ sốđiểm chỉ sốcổ phiếukhông có
Phái sinhđiểm chỉ sốhợp đồngVND 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_closebasis'index_point', còn basis_pct'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

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

Nội dung trang