Metadata-Version: 2.4
Name: finlens
Version: 1.5.0
Summary: Python client for Vietnamese stock market data — daily & intraday prices, investor flows, financial statements and macroeconomic indicators as pandas DataFrames.
Author: FinLens Team
License-Expression: MIT
Project-URL: Homepage, https://finlens.vn
Project-URL: Documentation, https://docs.finlens.vn/python-sdk
Project-URL: Changelog, https://docs.finlens.vn/python-sdk/changelog
Keywords: finlens,vietnam,vietnam-stock-market,vietnamese-stocks,stock-market-data,stock-api,financial-data,financial-statements,ohlcv,vnindex,pandas,market-data,macroeconomics,vietnam-macro,cpi,exchange-rate,interest-rates,import-export,api-client
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas>=3.0.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: packaging>=23.0
Requires-Dist: ta-lib<0.8,>=0.7.1
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Requires-Dist: hypothesis>=6.100; extra == "dev"
Requires-Dist: ruff==0.16.1; extra == "dev"
Requires-Dist: mypy==2.3.0; extra == "dev"
Requires-Dist: pandas-stubs>=2.1; extra == "dev"
Requires-Dist: nox>=2024.4.15; extra == "dev"
Requires-Dist: pyyaml>=6.0; extra == "dev"
Requires-Dist: jinja2>=3.1; extra == "dev"
Provides-Extra: cache
Requires-Dist: pyarrow>=15.0; extra == "cache"
Dynamic: license-file

# finlens — Dữ liệu chứng khoán Việt Nam cho Python

[![PyPI](https://img.shields.io/pypi/v/finlens.svg)](https://pypi.org/project/finlens/)
[![Python](https://img.shields.io/pypi/pyversions/finlens.svg)](https://pypi.org/project/finlens/)
[![License](https://img.shields.io/pypi/l/finlens.svg)](https://opensource.org/license/mit)

**Thư viện Python lấy dữ liệu thị trường chứng khoán Việt Nam** — giá cuối ngày
và trong phiên, khớp lệnh từng lệnh (tick-by-tick), dòng tiền theo nhóm nhà đầu
tư, báo cáo tài chính, chỉ số vĩ mô và danh mục mã — trả về thẳng dưới dạng
`pandas.DataFrame`, kèm **135 hàm chỉ báo kỹ thuật TA-Lib** chạy thẳng trên
frame nhiều mã.

Phủ **HOSE (HSX), HNX và UPCOM**: 1.646 cổ phiếu và chứng chỉ quỹ, 341 chứng
quyền có bảo đảm còn hạn (750 kể cả đã đáo hạn), hợp đồng phái sinh `VN30F1M`, và
các chỉ số `VNINDEX`, `VN30`, `HNX-INDEX`, `UPCOM-INDEX`.

> *Python library for Vietnam stock market data — daily and intraday OHLCV,
> tick-by-tick trades, foreign investor flows, financial statements, plus 135
> TA-Lib technical indicators. Covers HOSE, HNX and UPCOM. Returns pandas
> DataFrames.*

```bash
pip install finlens
```

```python
import finlens

client = finlens.client(api_key="flk_...")  # hoặc đặt biến FINLENS_API_KEY

df = client.eod.stock.ohlcv("HPG,VCB,FPT", start="2026-01-01", end="2026-01-05")
print(df)
#   symbol       date   open   high    low  close       volume
# 0    FPT 2026-01-05  94.40  94.40  91.93  93.71    7129300.0
# 1    HPG 2026-01-05  23.57  23.70  22.94  23.17   59427911.0
# 2    VCB 2026-01-05  ...
```

Giá ở đây tính bằng **nghìn đồng** — `23.17` nghĩa là 23.170 VND.

---

## Mục lục

- [Vì sao dùng finlens](#vì-sao-dùng-finlens)
- [Cài đặt và khoá API](#cài-đặt-và-khoá-api)
- [Lấy được những dữ liệu gì](#lấy-được-những-dữ-liệu-gì)
- [Chỉ báo kỹ thuật](#chỉ-báo-kỹ-thuật)
- [Đơn vị — đọc trước khi tính toán](#đơn-vị--đọc-trước-khi-tính-toán)
- [Tra cứu nhanh](#tra-cứu-nhanh)
- [Xử lý lỗi](#xử-lý-lỗi)
- [Async](#async)
- [Câu hỏi thường gặp](#câu-hỏi-thường-gặp)
- [Nâng cấp từ 0.1.x](#nâng-cấp-từ-01x)
- [Tài liệu và hỗ trợ](#tài-liệu-và-hỗ-trợ)
- [Giấy phép](#giấy-phép)

---

## Vì sao dùng finlens

**Trả về `pandas.DataFrame` trần, không phải wrapper.** Ghi được `to_parquet`,
nối được `pd.concat`, dùng được mọi tutorial pandas bạn từng đọc.

**Nhiều mã trong một request.** `ohlcv("HPG,VCB,FPT,...")` với 50 mã là **một**
lời gọi HTTP chứ không phải 50 lời gọi tuần tự.

**Đơn vị được khai báo, không phải đoán.** Mỗi frame mang theo
`df.attrs["finlens"]["units"]`. Giá cổ phiếu Việt Nam thường ghi bằng **nghìn
đồng** còn chứng quyền bằng **đồng** — nhầm chỗ này sai đúng 1000 lần và không
có lỗi nào báo. Thư viện tách chúng thành các namespace riêng nên một lời gọi
không bao giờ trả về bảng trộn hai đơn vị.

**Kết quả rỗng vẫn đúng cột, đúng kiểu.** Ngày thị trường nghỉ, `df["close"]`
vẫn chạy — không `KeyError`.

**Type hint đầy đủ, docstring tiếng Việt.** Autocomplete và `help()` hoạt động;
`mypy --strict` chạy sạch.

---

## Cài đặt và khoá API

```bash
pip install finlens
```

Yêu cầu **Python 3.11+** và **pandas 3.0+**. Từ bản 1.2.0, `pip install` kéo thêm
`ta-lib` — nó là nền của phần chỉ báo kỹ thuật.

### Lấy khoá API

Khoá có dạng `flk_...` và có **hai** đường tạo. Hướng dẫn đầy đủ ở
[docs.finlens.vn/python-sdk/cai-dat](https://docs.finlens.vn/python-sdk/cai-dat).

> ⚠️ **Mỗi tài khoản chỉ có MỘT khoá.** Khoá thuộc **tài khoản**, không thuộc
> thiết bị. Tạo khoá mới — ở extension hay trên web — **thu hồi ngay** khoá cũ, và
> mọi nơi đang dùng nó (máy khác, file cấu hình, tác vụ định kỳ) sẽ ngừng chạy.
>
> Khoá mới chỉ hiển thị **đúng một lần**. Về sau máy chủ chỉ trả lại 12 ký tự đầu
> qua `client.whoami()["key"]["prefix"]`.

**Cách 1 — trong extension VS Code (khuyến nghị).**

![Hộp thoại Tạo API key mới trong extension FinLens của VS Code, cảnh báo khoá hiện tại sẽ bị thu hồi](https://raw.githubusercontent.com/manhhung0221/finlens-vscode/main/docs/pypi/apikey-extension.png)

Mở **FinLens** trên Activity Bar → **Trang chủ** → nút **API key** → **Thu hồi và
tạo mới**.

Đường này được khuyến nghị vì khoá lưu thẳng vào **VS Code SecretStorage** —
không nằm trong file, không nằm trong code, không hiện ở cell notebook nào.

Để `finlens.client()` ở notebook và terminal đọc được khoá đó **tự động**, chạy
lệnh **FinLens: Ghi API key ra file cấu hình (cho notebook và terminal ngoài)**.
Extension ghi khoá vào
`finlens.toml`, và thư viện đọc nó ở tầng thứ ba của thứ tự ưu tiên bên dưới —
bạn không phải đặt biến môi trường bằng tay.

**Cách 2 — trên finlens.vn.**

![Cửa sổ Cài đặt tài khoản trên finlens.vn, mục API key dùng cho thư viện Python finlens](https://raw.githubusercontent.com/manhhung0221/finlens-vscode/main/docs/pypi/apikey-webapp.png)

Đăng nhập [finlens.vn](https://finlens.vn) → **Cài đặt tài khoản** → mục **API
key** → **Tạo API key mới**, rồi sao chép khoá.

### Ba cách đưa khoá vào client

```python
import finlens

# Cách 1 — truyền thẳng
client = finlens.client(api_key="flk_...")

# Cách 2 — biến môi trường FINLENS_API_KEY (khuyến nghị)
client = finlens.client()

# Dùng như context manager để đóng kết nối gọn gàng
with finlens.client() as client:
    df = client.eod.stock.ohlcv("HPG")
```

**Cách 3 — file cấu hình.** Tiện khi bạn dùng nhiều notebook và không muốn đặt
biến môi trường ở mỗi nơi. Đây cũng là file mà extension VS Code ghi giúp bạn:

```toml
# ./finlens.toml, hoặc %APPDATA%\finlens\config.toml (Windows)
#                hoặc ~/.config/finlens/config.toml (macOS, Linux)
[default]
api_key = "flk_..."
```

⚠️ **File đầu tiên tìm thấy là file duy nhất được đọc — không merge.** Một
`./finlens.toml` trong thư mục dự án sẽ **che hoàn toàn** file cấu hình máy của
bạn, kể cả những khoá nó không khai. Nếu thiếu khoá, thông báo lỗi sẽ nói rõ nó
đã tìm ở đâu và file nào che file nào.

Thứ tự ưu tiên đầy đủ: `api_key=` → `FINLENS_API_KEY` → file cấu hình.

⚠️ **Đừng viết khoá thẳng vào code rồi đẩy lên Git.** Trong notebook dùng chung,
`finlens.client()` không tham số là an toàn nhất: khoá không xuất hiện ở cell nào,
nên nó cũng không nằm trong file `.ipynb` bạn gửi đi. Nếu dùng `./finlens.toml`
trong thư mục dự án, thêm nó vào `.gitignore`.

Tạo client **không chạm mạng** — an toàn để đặt ở cell đầu notebook hoặc trong
`__init__` của một lớp.

```python
client.whoami()  # gói dịch vụ, hạn mức, ngày hết hạn
client.status()  # trạng thái service (không cần khoá)
```

---

## Lấy được những dữ liệu gì

### Giá cuối ngày (EOD)

```python
client.eod.stock.ohlcv("HPG", start="2020-01-01", interval="1w")
client.eod.index.ohlcv("VNINDEX")
client.eod.derivative.ohlcv("VN30F1M")
client.eod.warrant.ohlcv("CHPG2628")
client.eod.sector.ohlcv("8355")  # chỉ số ngành ICB
```

Cổ phiếu có từ **2007**, chỉ số ngành từ **2000**. `interval` nhận `1d`, `1w`,
`1mo`, `3mo`, `6mo`, `1y` — gộp nhóm chạy ở server.

Giá cổ phiếu mặc định **đã điều chỉnh quyền**; thêm `adjusted=False` để lấy giá
thô đúng như phiên hôm đó.

### Trong phiên và tick-by-tick

```python
client.intraday.stock.ohlcv("HPG", interval="5min")
client.intraday.stock.ticks("HPG", date="2026-08-03")  # từng lệnh khớp
client.intraday.stock.net_active_value("HPG", interval="1h")  # mua/bán chủ động
```

Tick có từ **2022**. `interval` trong phiên: `1min`, `5min`, `15min`, `30min`,
`1h`, `4h`.

Cột `side` của tick có **ba** giá trị: `buy` (bên mua nâng giá chạm bên bán),
`sell` (bên bán hạ giá chạm bên mua), và `auction` (khớp lệnh định kỳ ATO/ATC).
Phiên định kỳ không có bên chủ động nên xếp nó vào mua hay bán đều sai — nó
chiếm 1,4%–8,6% khối lượng tuỳ mã, quá lớn để giấu.

`ticks()` lấy **một mã, một phiên** mỗi lần gọi: một phiên phái sinh sôi động là
hơn 90.000 dòng.

### Dòng tiền theo nhóm nhà đầu tư

```python
client.eod.stock.investor.flow("HPG", group="foreign")
client.eod.stock.investor.breakdown("HPG")  # tất cả các nhóm
client.eod.sector.investor.flow("8355")  # theo ngành ICB
```

| Nhóm | Có từ | Phạm vi |
|---|---|---|
| `foreign` | 2010 | cả ba sàn |
| `foreign_individual`, `foreign_institutional` | 2024 | HOSE |
| `local_individual`, `local_institutional` | 2024 | HOSE |
| `proprietary` (tự doanh) | 2022 | cả ba sàn |

**Bốn nhóm chi tiết cộng lại bằng 0** — mua ròng của nhóm này là bán ròng của
nhóm kia. `foreign` và `proprietary` đến từ nguồn khác và nằm trên trục riêng;
`meta.additive_groups` trong response nói rõ nhóm nào cộng được với nhau.

Ở **hợp đồng phái sinh** chỉ có `foreign` và `proprietary`; bốn nhóm chi tiết
không tồn tại và kiểu của tham số đã chặn từ lúc gõ code:

```python
client.eod.derivative.investor.flow("VN30F1M", group="proprietary")
client.eod.derivative.investor.breakdown("VN30F1M")  # cả hai nhóm
```

### Sổ lệnh đặt và khối lượng chủ động

```python
client.eod.stock.supply_demand("HPG")  # lệnh ĐẶT vào sổ
client.eod.stock.active_volume("HPG")  # khối lượng khớp chủ động
client.eod.derivative.active_volume("VN30F1M")
```

⚠️ **Hai bảng này không trừ được cho nhau, và không trừ được cho `ohlcv()`.**

`supply_demand()` là **lệnh đặt**, không phải lệnh khớp: trung vị
`buy_order_volume / volume` là **2,756 lần**, và 99,77% số dòng có khối lượng
đặt lớn hơn hoặc bằng khối lượng khớp. Đó là lý do mọi cột mang chữ `_order_`.
Ba cột `_count` là `Int64` nullable — server phát `null` ở 11,38% số dòng, nên
kiểm bằng `.isna()` chứ đừng so với `0`. Chỉ có ở `client.eod.stock`.

`active_volume()` là **khối lượng**, hai rổ, **không có cột tiền nào**. Nó khác
hẳn `client.intraday.*.net_active_value()` vốn có ba rổ và tính bằng VND — cùng
một phiên VN30F1M, hai đại lượng lệch nhau 57,5% · 520% · 30,1%.

### Chênh lệch phái sinh và chỉ số cơ sở

```python
client.eod.derivative.basis("VN30F1M")  # theo phiên
client.intraday.derivative.basis("VN30F1M")  # bước 1 phút
```

```
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ố là **giá chỉ số**, không phải giá hợp đồng — 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. Không method nào có
`interval`: gộp một chênh lệch qua nhiều bước không có nghĩa hiển nhiên nào.

Mã phái sinh không có chỉ số cơ sở đi vào phần lỗi **theo từng mã**
(`FL_DATA_NO_UNDERLYING`) và không làm hỏng cả lời gọi.

### Báo cáo tài chính

```python
client.financials.statement("HPG", kind="balance_sheet", period="quarterly")
client.financials.periods("HPG")  # kỳ nào có số liệu
client.financials.line_items(com_type="NH", kind="balance_sheet")
```

Có từ **2004**. Bốn loại hình doanh nghiệp (`CT` phi tài chính, `NH` ngân hàng,
`CK` chứng khoán, `BH` bảo hiểm) có cây chỉ tiêu khác nhau, nhưng frame ở dạng
long và mỗi dòng mang `company_type` của chính nó — nên
`statement(["HPG", "VCB"])` chạy được dù hai mã khác loại hình.

### Tra cứu danh mục

```python
client.meta.symbols()  # 1.645 mã
client.meta.symbols(exchange="HOSE")  # 431 mã
client.meta.symbols(icb="8300")  # toàn ngành ngân hàng
client.meta.symbols(kind="fund")  # 24 chứng chỉ quỹ niêm yết
client.meta.sectors(level=2)  # 19 ngành ICB cấp 2
client.meta.sectors(level=4)  # 106 ngành ICB cấp 4
client.meta.warrants(underlying="HPG")  # chứng quyền của HPG
```

Tham số `icb=` nhận **cả mã cấp 2 lẫn cấp 4** — bạn không cần biết mã mình cầm
thuộc cấp nào.

```python
# Lấy danh sách mã để lặp
tickers = client.meta.symbols(exchange="HOSE")["symbol"].tolist()
```

### Vĩ mô

Chỉ số thống kê, nghiệp vụ thị trường mở và xuất nhập khẩu — nguồn là Tổng cục
Thống kê và Ngân hàng Nhà nước. **3.348 chuỗi** chỉ tiêu, cập nhật hằng ngày.

```python
# Tra mã trước, rồi mới hỏi số — mã không đoán được từ tên
ds = client.macro.indicators(topic="cpi", freq="monthly")
df = client.macro.series(ds["code"].tolist()[:5])

client.macro.series("ty_gia_trung_tam_daily")  # tỷ giá trung tâm, VND
client.macro.series("gia_vang_giao_ngay_daily")  # giá vàng, USD/Ounce
client.macro.omo(kind="net_pump")  # NHNN bơm hút ròng
client.macro.trade(flow="export", by="country")  # xuất khẩu theo đối tác
```

⚠️ **Đây là namespace duy nhất mà `unit` là một CỘT chứ không phải thuộc tính
của cả bảng.** Hỏi hai chỉ tiêu bất kỳ là có thể nhận `%` nằm cạnh `USD/thùng`
trong cùng cột `value` — mọi namespace khác không bao giờ trộn đơn vị vì mỗi
loại tài sản nằm ở một namespace riêng. Đọc `unit` theo **từng dòng**.

⚠️ Cột `date` là **cuối kỳ quan sát**, không phải một phiên giao dịch; cột
`period` đi kèm mới nói kỳ nào (`"7-2026"`, `"Q1-2026"`).

### Trái phiếu doanh nghiệp

**6.820 lô** trái phiếu của **940 tổ chức phát hành**: điều khoản từng lô, dư nợ
đang lưu hành, và hồ sơ tổ chức đã tổng hợp sẵn.

```python
client.bonds.list()  # trọn danh mục, một request
client.bonds.list(sector="real_estate", outstanding=True)  # còn lưu hành
client.bonds.list(symbol="HPG")  # đi bằng mã cổ phiếu
client.bonds.list(outstanding=True, maturity_to="2027-08-11")  # tường đáo hạn
client.bonds.issuers(has_stock=True)  # 128 tổ chức có niêm yết
```

⚠️ **Đây là ẢNH CHỤP, không phải chuỗi thời gian.** Nguồn crawl một lần mỗi ngày
rồi ghi đè — không có lịch sử dư nợ. `matured=` so với mốc ở
`df.attrs["finlens"]["source_updated_at"]`, **không** so với đồng hồ của bạn.

⚠️ **Tiền đi theo CẶP `_mvnd` / `_usd`**, mỗi dòng đúng một vế khác `null` và
`currency` quyết định vế nào. Hai vế **không cộng được với nhau** — nguồn không
chứa tỷ giá nào, nên "tổng dư nợ TPDN" tính từ `outstanding_value_mvnd` bỏ sót
phần USD, xấp xỉ 4,6% thị trường. Thang đo là **triệu VND**.

⚠️ **Ba không gian mã, không cái nào thay được cái nào:** `codes=` là mã **lô**
(`TPTTB2018/3Y` — **đừng viết hoa**, 2.479 mã chứa ký tự ngoài `[A-Z0-9]`),
`issuer=` là mã **tổ chức** của nguồn trái phiếu, `symbol=` là mã **cổ phiếu**.
Ở `issuers()` thì `codes=` mang mã **tổ chức**.

⚠️ Gõ `currency="VND"`, **không phải `"VNĐ"`** — nguồn lưu chuỗi có dấu, máy chủ
dịch giúp, và gõ chuỗi của nguồn nhận một lỗi 400.

---

## Chỉ báo kỹ thuật

**135 hàm TA-Lib**, ở hai tầng: `df.finlens.*` chạy trên `DataFrame` và **tự
tách theo mã**, còn `finlens.ta.*` bám sát TA-Lib — mảng vào, mảng ra.

```python
import finlens

client = finlens.client()
df = client.eod.stock.ohlcv(["HPG", "VCB"], start="2024-01-01")

df = df.finlens.rsi(14).finlens.macd().finlens.bbands(20)
print([c for c in df.columns if c not in ("symbol", "date")])
# ['open', 'high', 'low', 'close', 'volume',
#  'rsi_14', 'macd_12_26_9', 'macdsignal_12_26_9', 'macdhist_12_26_9',
#  'upperband_20_2_2_0', 'middleband_20_2_2_0', 'lowerband_20_2_2_0']
```

⚠️ **Đây là lý do tầng `df.finlens.*` tồn tại.** `talib.RSI(df["close"])` trên
một frame hai mã cho cửa sổ 14 phiên đầu của mã sau ăn 13 giá cuối của mã
trước. Đo trên frame HPG+VCB 80 dòng: **sai 40/80 dòng**, mọi giá trị sai đều
nằm trong khoảng 0–100 hợp lệ, không một cảnh báo nào. `df.finlens.rsi(14)` tự
dò cột khoá (`symbol`, `icb`, `code`) và tính riêng từng nhóm; `by=None` nếu
frame của bạn thật sự là một chuỗi giá duy nhất.

Mọi method trả về một **bản sao** kèm cột mới — không sửa tại chỗ, nên nối
chuỗi được. Tên cột mang theo tham số, nên `sma(20)` và `sma(50)` là hai cột
chứ không đè lên nhau.

### Mẫu nến

```python
df.finlens.patterns("doji")  # dạng DÀI, chỉ gồm các lần bắt được
df.finlens.patterns(["engulfing", "morningstar"], direction="tang")
df.finlens.pattern.cdldoji()  # dạng RỘNG, thêm một cột `cdldoji`
```

`patterns()` trả về `symbol | date | pattern | ten_mau | signal | direction`.
Tên nhận cả `"CDLDOJI"`, `"cdldoji"` và `"doji"`.

⚠️ **Cột `signal` không chỉ có `±100`.** `CDLHIKKAKE` và `CDLHIKKAKEMOD` ra
thêm `±200` cho thanh xác nhận, nên `df[df.signal == 100]` âm thầm đánh rơi
chúng. Lọc bằng `df.signal > 0`, hoặc dùng cột `direction` (`"tang"` /
`"giam"`) đã suy sẵn từ dấu.

⚠️ **Quét cả 61 mẫu cho ra 1,8 dòng kết quả trên mỗi dòng đầu vào.** Đo trên
500 mã × 1.500 phiên (750.000 dòng): 1.349.970 dòng, 2,22 giây, 312 MiB. Nêu
tên mẫu trong `which=` cắt được **10 lần** bộ nhớ, chứ không phải vài phần trăm.

### Tầng bám sát TA-Lib

```python
import finlens

close = df.loc[df["symbol"] == "HPG", "close"]

finlens.ta.RSI(close, timeperiod=14)  # Series vào → Series ra, giữ index
finlens.ta.MACD(close)  # tuple ba Series
finlens.ta.pattern.CDLDOJI(df["open"], df["high"], df["low"], df["close"])
```

Tên hàm, tên tham số và giá trị mặc định giữ **nguyên của TA-Lib**, nên code
TA-Lib có sẵn chạy được sau khi đổi mỗi dòng `import`. Khác đúng ba chỗ, cả ba
để chặn một cách hỏng im lặng:

- **Tham số là keyword-only.** `RSI(close, 14)` ném `TypeError`. TA-Lib cho
  phép nó, và `MACD(c, 26, 12, 9)` thì đảo `fastperiod` với `slowperiod` rồi
  trả về một chỉ báo khác mà không báo gì.
- **Mọi lỗi là `finlens.FinLensError`.** TA-Lib ném `Exception` **trần**.
- **`timeperiod=14.5` bị từ chối.** TA-Lib chạy và cắt phần thập phân trong im
  lặng, nên sau lời gọi không còn gì phân biệt được hai ý định đó.

74 chỉ báo ở `finlens.ta`, 61 mẫu nến ở `finlens.ta.pattern`. Tầng
`df.finlens.*` có 73 chỉ báo — `MAVP` vắng mặt vì nó cần một *mảng chu kỳ theo
từng thanh* chứ không phải một cột giá.

### Bốn cái bẫy chung cho cả hai tầng

- **Warm-up không phải lỗi, và hai loại hàm biểu diễn nó khác nhau.** Chỉ báo
  ra `NaN` (`RSI` với `timeperiod=14` là đúng 14 dòng đầu **mỗi mã**); mẫu nến
  ra số **`0`**, không phân biệt được với "đã quét và không có mẫu". Nhóm ngắn
  hơn warm-up ra **toàn** `NaN` / `0` và TA-Lib không báo lỗi — ở tầng
  `df.finlens.*` nó thành một `finlens.DataQualityWarning`.
- **`NaN` ở giữa chuỗi lan tới hết chuỗi**, vĩnh viễn. Đo: một `NaN` ở dòng 30
  của 60 làm dòng 30–59 toàn `NaN`. Thư viện cảnh báo chứ không tự chữa —
  `ffill` là bịa số, `dropna` là đổi cửa sổ, và cả hai là quyết định của người
  phân tích.
- **24 trên 74 chỉ báo có trạng thái không ổn định**, tức kết quả phụ thuộc chỗ
  bạn bắt đầu chuỗi. `RSI(c)[200:]` so với `RSI(c[200:])` lệch tới **2,02
  điểm**, và phải tới phần tử thứ **85** chênh lệch mới xuống dưới 0,01. `SMA`
  thì không (lệch 7 × 10⁻¹⁴). Đổi `start=` của lời gọi dữ liệu là đổi con số
  bạn nhận về; docstring từng hàm nói rõ hàm nào.
- **Dữ liệu chưa sắp xếp theo thời gian cho ra một dãy số khác hẳn.** Ở tầng
  `df.finlens.*` điều đó được xử lý — chỉ báo tính trên bản đã sắp rồi trả kết
  quả về đúng vị trí dòng gốc, kèm cảnh báo, và thứ tự dòng bạn nhận về không
  đổi. Ở tầng `finlens.ta.*` thì không có ai đứng giữa.

`df.finlens` được đăng ký khi `finlens.accessor` được nạp, và
`finlens.client()` nạp nó. Nếu bạn dựng `DataFrame` từ file mà không tạo client
thì cần `import finlens.accessor` một lần.

Đó là cái giá của một thứ đáng giữ: **`import finlens` không kéo theo `talib`,
`pandas` hay `numpy`** — sau khi chạm cả vào `finlens.ta`, không tên nào trong
ba tên đó có mặt trong `sys.modules`; chúng chỉ vào khi một hàm thật sự được
gọi. Riêng `import talib` là khoảng nửa giây, và nó tự kéo pandas theo.

---

## Đơn vị — đọc trước khi tính toán

Đây là nguồn lỗi số một khi làm việc với dữ liệu chứng khoán Việt Nam, và nó
**sai âm thầm**: không exception nào, chỉ là một con số sai.

| Loại | Cột giá | Khối lượng | Giá trị tiền |
|---|---|---|---|
| Cổ phiếu, ETF, chứng chỉ quỹ | **nghìn VND** (`22.3` = 22.300 đ) | cổ phiếu | VND |
| Chỉ số | điểm chỉ số | cổ phiếu | — |
| Phái sinh | điểm chỉ số | **hợp đồng** | VND |
| Chứng quyền | **VND thô** | chứng quyền | VND |

Luôn đọc thay vì giả định:

```python
df.attrs["finlens"]["units"]  # {'close': 'kVND', 'volume': 'share', ...}
df.attrs["finlens"]["price_basis"]  # 'adjusted' hoặc 'raw'
df.attrs["finlens"]["as_of"]  # mốc nước của dữ liệu
```

⚠️ `DataFrame.attrs` **không sống sót** qua `pd.concat` hay `merge` của pandas.
Đọc đơn vị trước khi ghép frame.

---

## Tra cứu nhanh

| Bạn muốn | Gọi |
|---|---|
| Giá VNINDEX theo tháng | `client.eod.index.ohlcv("VNINDEX", interval="1mo")` |
| Khối ngoại mua ròng HPG | `client.eod.stock.investor.flow("HPG")` |
| Từng lệnh khớp một phiên | `client.intraday.stock.ticks("HPG", date="2026-08-03")` |
| Lệnh đặt vào sổ theo phiên | `client.eod.stock.supply_demand("HPG")` |
| Chênh lệch VN30F1M với VN30 | `client.eod.derivative.basis("VN30F1M")` |
| Cân đối kế toán theo quý | `client.financials.statement("HPG", kind="balance_sheet", period="quarterly")` |
| Mọi mã ngành ngân hàng | `client.meta.symbols(icb="8300")` |
| Chứng quyền còn hạn | `client.meta.warrants()` |
| Giá thô, chưa điều chỉnh | `client.eod.stock.ohlcv("HPG", adjusted=False)` |
| CPI, tỷ giá, lãi suất theo kỳ | `client.macro.series("ty_gia_trung_tam_daily")` |
| Cán cân thương mại theo tháng | `client.macro.trade(flow="balance")` |
| RSI, MACD trên frame nhiều mã | `df.finlens.rsi(14).finlens.macd()` |
| Mẫu nến của cả frame | `df.finlens.patterns("engulfing")` |
| Một chỉ báo trên một mảng | `finlens.ta.ATR(high, low, close, timeperiod=14)` |

Mọi phương thức đều nhận `refresh=True` để bỏ qua cache, và `on_error="raise"`
để một mã lỗi làm cả lời gọi thất bại thay vì chỉ cảnh báo.

---

## Xử lý lỗi

Mọi lỗi kế thừa `finlens.FinLensError` và mang theo `.code`, `.request_id`,
`.doc_url`.

```python
import finlens

try:
    df = client.eod.stock.ohlcv("HPG")
except finlens.RateLimitError as e:
    print(f"Chờ {e.retry_after} giây")
except finlens.DailyQuotaExceededError as e:
    print(f"Hết hạn mức ngày, mở lại lúc {e.resets_at}")
except finlens.InvalidSymbolError:
    print("Mã không tồn tại")
except finlens.FinLensError as e:
    print(f"{e.code}: {e}")
```

Cây ngoại lệ:

```
FinLensError
├── AuthError          InvalidApiKeyError · ApiKeyExpiredError · AccountExpiredError
├── TierError          DatasetNotInTierError · SymbolNotInTierError
├── QuotaError         RateLimitError · DailyQuotaExceededError
├── ValidationError    InvalidSymbolError · InvalidDateRangeError · InvalidIntervalError
├── TransportError     ConnectionFailedError · TlsVerificationError · RequestTimeoutError
└── DataError          SchemaMismatchError · NoDataError
```

`ValidationError` cũng kế thừa `ValueError`, nên `except ValueError` vẫn bắt được.

**Một mã lỗi không làm mất các mã còn lại.** Mặc định `on_error="warn"`: bạn
nhận về những mã thành công kèm một cảnh báo, chi tiết ở
`df.attrs["finlens"]["failed"]`.

---

## Async

Mọi thứ có bản async với cùng chữ ký:

```python
import asyncio
import finlens


async def main():
    async with finlens.AsyncClient(api_key="flk_...") as client:
        df = await client.eod.stock.ohlcv("HPG,VCB")


asyncio.run(main())
```

Bản đồng bộ và bất đồng bộ dùng **chung một lõi**, nên không có chuyện một bên
được sửa bug còn bên kia thì không.

---

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

**Giá cổ phiếu tính bằng đơn vị gì?**
Nghìn đồng. `22.3` nghĩa là 22.300 VND. Chứng quyền thì ngược lại — VND thô.
Luôn đọc `df.attrs["finlens"]["units"]`.

**Dữ liệu có từ năm nào?**
Giá cuối ngày cổ phiếu từ 2007, chỉ số ngành từ 2000, khối ngoại từ 2010, báo
cáo tài chính từ 2004, tick trong phiên từ 2022. Nhóm nhà đầu tư chi tiết (cá
nhân/tổ chức, trong nước/nước ngoài) từ 2024 và chỉ có ở HOSE.

**Lấy được dữ liệu của phiên đang chạy không?**
Được. Dữ liệu trong phiên cập nhật liên tục và `meta.as_of` cho biết mốc nước.
Lưu ý giá trong phiên là **giá thô**, chưa điều chỉnh quyền — khác với EOD.

**Lấy được bao nhiêu mã một lần?**
Tuỳ gói dịch vụ, xem `client.limits()`. Thư viện tự chia nhỏ và gọi song song,
bạn cứ truyền cả danh sách. Riêng `ticks()` là một mã một phiên.

**Có chỉ báo kỹ thuật không?**
Có, 135 hàm TA-Lib — xem [Chỉ báo kỹ thuật](#chỉ-báo-kỹ-thuật). `ta-lib` là
phụ thuộc bắt buộc nên `pip install finlens` là đủ, không cần extra nào. Nếu
bạn đã có sẵn code gọi `talib` thì đổi mỗi dòng `import` là chạy: tên hàm, tên
tham số và giá trị mặc định giữ nguyên.

**Có sổ lệnh (order book) không?**
Không. `ticks()` trả **lệnh đã khớp**, không phải độ sâu sổ lệnh. Giá đặt và
khối lượng chờ theo bậc không có trong nguồn dữ liệu.

**Có dữ liệu quỹ mở, trái phiếu, hàng hoá không?**
Chứng chỉ quỹ **niêm yết** (`kind="fund"`) thì có. **Giá hàng hoá và tỷ giá thì
có, qua `client.macro.series()`** — vàng giao ngay, dầu Brent, dầu WTI, tỷ giá
trung tâm, lãi suất liên ngân hàng, lợi suất trái phiếu chính phủ:

```python
client.macro.series("gia_vang_giao_ngay_daily")  # USD/Ounce
client.macro.series("dau_tho_brent_daily")  # USD/thùng
```

Quỹ mở, NAV, và giá từng mã trái phiếu doanh nghiệp thì chưa có.

**Cache hoạt động thế nào?**
Tự động. Dữ liệu lịch sử cache 7 ngày, phiên gần nhất 60 giây, danh mục và cây
ngành 24 giờ. `refresh=True` để bỏ qua, `client.cache.stats()` để xem.

**Chạy sau proxy doanh nghiệp hoặc phần mềm diệt virus?**
Nếu gặp `TlsVerificationError`, trỏ tới CA bundle của tổ chức bạn:
`finlens.client(ca_bundle="/đường/dẫn/ca.pem")` hoặc đặt biến môi trường
`FINLENS_CA_BUNDLE`.

---

## Nâng cấp từ 0.1.x

Phiên bản 1.0 là bản viết lại và có thay đổi phá vỡ tương thích. Danh sách
đầy đủ nằm ở [changelog](https://docs.finlens.vn/python-sdk/changelog). Đáng chú ý nhất:

- Tên cột dùng `snake_case` — `Date` thành `date`.
- `interval="1M"` bị từ chối vì nhập nhằng giữa *một tháng* và *một phút*; dùng
  `1mo` hoặc `1min`.
- `net_active_value()` trước đây trả ba đơn vị khác nhau dưới cùng một tên cột;
  nay luôn là VND và có `meta.value_unit` khai rõ.
- Mọi tham số sau mã chứng khoán là **keyword-only**.

---

## Tài liệu và hỗ trợ

**Tài liệu**

- Hướng dẫn thư viện: [docs.finlens.vn/python-sdk](https://docs.finlens.vn/python-sdk)
- Cài đặt và khoá API: [docs.finlens.vn/python-sdk/cai-dat](https://docs.finlens.vn/python-sdk/cai-dat)
- Changelog: [docs.finlens.vn/python-sdk/changelog](https://docs.finlens.vn/python-sdk/changelog)

**Extension VS Code** — dựng lời gọi bằng giao diện, xem trước dữ liệu ngay trong
editor, và tạo khoá API mà không phải rời khỏi editor:

- Hướng dẫn sử dụng: [docs.finlens.vn/vscode](https://docs.finlens.vn/vscode)
- Cài đặt: [marketplace.visualstudio.com](https://marketplace.visualstudio.com/items?itemName=finlens.finlens)

**Báo lỗi và hỗ trợ**

- Lỗi của thư viện: [mở issue trên GitHub](https://github.com/manhhung0221/finlens-python-examples/issues)
- Hỏi đáp và tài khoản: client@finlens.vn
- Trang chủ: [finlens.vn](https://finlens.vn)

Khi mở issue, kèm theo `finlens.build_info()` và `request_id` trong thông báo lỗi
— hai thứ đó cho biết chính xác bản build nào và request nào, và không có chúng
thì gần như không lần lại được.

```python
>>> finlens.build_info()
{'version': 'X.Y.Z', 'commit': 'a1b2c3d', 'built_at': '...', 'cython': '3.2.9', ...}
```

---

## Giấy phép

MIT — xem [toàn văn giấy phép](https://opensource.org/license/mit). File `LICENSE` cũng đi kèm trong gói.

<!--
Từ khoá: dữ liệu chứng khoán Việt Nam, chứng khoán Việt Nam Python, API chứng
khoán, thư viện Python chứng khoán, VNINDEX, VN30, VN30F1M, HOSE, HSX, HNX,
UPCOM, giá cổ phiếu lịch sử, dữ liệu intraday, tick by tick, khớp lệnh, khối
ngoại, dòng tiền nhà đầu tư, tự doanh, báo cáo tài chính, ngành ICB, chứng quyền
có bảo đảm, phái sinh, pandas, DataFrame, phân tích định lượng, backtest, chỉ
báo kỹ thuật, TA-Lib, RSI, MACD, Bollinger Bands, mẫu hình nến, phân tích kỹ
thuật.
Vietnam stock market data Python, Vietnamese stocks API, HOSE HNX UPCOM data,
VNINDEX historical data, intraday tick data Vietnam, foreign investor flows,
financial statements Vietnam, pandas DataFrame, quantitative finance Vietnam,
TA-Lib technical indicators, candlestick patterns.
-->
