Chỉ báo kỹ thuật trong Python: RSI, MACD, Bollinger
Tính RSI, MACD, Bollinger Bands và quét 61 mẫu nến trên dữ liệu chứng khoán Việt Nam bằng finlens: df.finlens.rsi(14) tự tách theo mã, hoặc finlens.ta trên mảng.
Một chuỗi giá không tự nói nó đang mạnh hay yếu. finlens mang sẵn 135 hàm
của TA-Lib — 74 chỉ báo và 61 mẫu nến — nên bạn không phải tự cài thư viện C,
tự tách mã, hay tự nhớ tham số nào đi trước tham số nào.
Có hai tầng, và bạn gần như luôn muốn tầng thứ hai.
| Gọi | Trả lời câu hỏi |
|---|---|
df.finlens.rsi(14) | Thêm cột chỉ báo vào bảng giá tôi vừa lấy về |
df.finlens.pattern.cdlengulfing() | Thêm cột tín hiệu của một mẫu nến |
df.finlens.patterns() | Quét cả 61 mẫu, trả về các lần bắt được |
finlens.ta.RSI(mang) | Tôi đã có sẵn mảng numpy, chỉ cần con số |
help(finlens.ta.RSI) | Hàm này cần gì, warm-up bao nhiêu, bẫy ở đâu |
Từ con số không tới một cột RSI của ba mã là bốn dòng:
import finlens
client = finlens.client()
df = client.eod.stock.ohlcv(["HPG", "VCB", "FPT"], start="2024-01-01")
df = df.finlens.rsi(14)symbol date close rsi_14
FPT 2026-08-10 71.8 60.392038
HPG 2026-08-10 22.1 50.407689
VCB 2026-08-10 60.3 59.158631Không dòng nào nhắc tới talib, và không dòng nào tách mã bằng tay.
Ba điều quyết định con số bạn nhận về
- Một cột giá gồm nhiều mã nối đuôi nhau cho ra số sai ở đầu mỗi mã. Đây
là lý do tầng
df.finlens.*tồn tại — nó tự tách theo cộtsymbol. Tầngfinlens.ta.*thì không biết gì về mã. NaNở đầu chuỗi không phải lỗi, đó là warm-up. Chuỗi ngắn hơn warm-up cho ra toànNaN— với mẫu nến thì toàn số0, không phân biệt được với "đã quét và không thấy gì".- 24 trên 74 chỉ báo đổi kết quả khi bạn đổi
start=, chứ không chỉ ở vùng warm-up.RSInằm trong số đó,SMAthì không.
Hai tầng, và khi nào dùng cái nào
df.finlens.* | finlens.ta.* | |
|---|---|---|
| Vào | DataFrame | mảng numpy hoặc Series |
| Ra | bản sao của frame, kèm cột mới | mảng, hoặc tuple mảng |
| Bề mặt | 73 chỉ báo + 61 mẫu nến + patterns() | 74 chỉ báo + 61 mẫu nến |
| Nhiều mã | tự tách theo symbol / icb | không biết mã là gì |
| Cảnh báo dữ liệu | có | không |
Tên ở tầng hai là tên ở tầng một viết thường, không một ngoại lệ nào:
RSI → rsi, HT_DCPERIOD → ht_dcperiod, MINUS_DI → minus_di,
CDLENGULFING → pattern.cdlengulfing. Gạch dưới giữ nguyên.
Đúng một hàm chỉ có ở tầng mảng: MAVP. Nó cần một mảng chu kỳ theo từng
thanh chứ không phải một cột giá, nên không có cách nào diễn đạt nó bằng một
tham số của accessor.
Dùng tầng mảng khi bạn đã cầm sẵn numpy — kết quả backtest, dữ liệu mô phỏng,
một Series bạn tự dựng. Mọi trường hợp còn lại dùng accessor.
`df.finlens` xuất hiện khi bạn tạo client
Accessor được gắn vào DataFrame lúc finlens.client() chạy. Với một frame bạn
tự dựng và chưa từng tạo client trong tiến trình đó, gọi import finlens.accessor
một lần là đủ:
import pandas as pd
import finlens.accessor
d = pd.DataFrame({"close": [float(i) for i in range(30)]})
d.finlens.rsi(14).columns.tolist()
# ['close', 'rsi_14']Đổi lại, import finlens không nạp TA-Lib: sau khi chạm vào finlens.ta,
sys.modules vẫn chưa có talib, pandas hay numpy. Chúng chỉ vào khi một
hàm thật sự được gọi.
df.finlens.* — đường chính
Mỗi chỉ báo là một phương thức nhận đúng các tham số của TA-Lib, cùng tên và cùng giá trị mặc định:
df.finlens.rsi(
timeperiod=14,
*,
price="close",
by=...,
suffix=None,
) -> DataFrame| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
| Tham số của TA-Lib | theo từng hàm | theo từng hàm | timeperiod, fastperiod, matype… — giữ nguyên tên gốc. |
price | str | "close" | Tên cột giá cần đọc. Hàm nhiều đầu vào có một tham số cho mỗi vai — open= · high= · low= · close= · volume= — thay cho price=; riêng OBV nhận price= cùng volume=. |
by | str hoặc None | tự dò | Cột khoá để tách nhóm. None tắt hẳn việc tách. |
suffix | str hoặc None | None | Đuôi thêm vào tên cột, để gọi cùng một chỉ báo hai lần. |
Trả về một bản sao. Frame gốc không bị đụng tới — sau df.finlens.rsi(14)
thì df vẫn đúng bảy cột như lúc lấy về.
Tên cột mang theo tham số
Đây là quy ước quan trọng nhất của tầng này: tên cột được dựng từ tên đầu ra cộng các tham số, nên hai lần gọi khác tham số cho ra hai cột khác nhau thay vì đè lên nhau.
| Lời gọi | Cột sinh ra |
|---|---|
rsi(14) | rsi_14 |
rsi(21) | rsi_21 |
sma(20) rồi sma(50) | sma_20 và sma_50 |
macd() | macd_12_26_9 · macdsignal_12_26_9 · macdhist_12_26_9 |
bbands(20) | upperband_20_2_2_0 · middleband_20_2_2_0 · lowerband_20_2_2_0 |
stoch() | slowk_5_3_0_3_0 · slowd_5_3_0_3_0 |
aroon(14) | aroondown_14 · aroonup_14 |
sar() | sar_0.02_0.2 |
obv() | obv |
pattern.cdlengulfing() | cdlengulfing |
Hàm không có tham số nào thì không có hậu tố nào — obv và cdlengulfing đứng
trần.
Vì tên cột đã mang đủ tham số, nối chuỗi là cách dùng tự nhiên:
df = df.finlens.rsi(14).finlens.macd()
list(df.columns)
# ['symbol', 'date', 'open', 'high', 'low', 'close', 'volume',
# 'rsi_14', 'macd_12_26_9', 'macdsignal_12_26_9', 'macdhist_12_26_9']Gọi lại đúng một chỉ báo với đúng tham số cũ thì ném lỗi
Không đè lên cột cũ, và cũng không lặng lẽ bỏ qua:
a = df.finlens.rsi(14)
a.finlens.rsi(14)
# ValidationError: [FL_TA_INPUT] `RSI` sẽ tạo cột rsi_14 nhưng frame đã có cột
# đó. Đè lên là làm mất số cũ trong im lặng — dùng `suffix=` để đặt tên khác,
# ví dụ `suffix='nhanh'`.Đường đi tiếp nằm ngay trong thông báo:
a.finlens.rsi(14, suffix="nhanh") # ['rsi_14', 'rsi_14_nhanh']Chỗ hay vấp nhất là chạy lại một cell notebook, hoặc một vòng lặp có tham số trùng.
Đọc cột khác close
price= đổi cột đầu vào của các chỉ báo một chuỗi; các chỉ báo nhiều chuỗi có
một tham số cho mỗi vai:
df.finlens.rsi(14, price="open") # đọc cột open thay cho close
df.finlens.atr(14) # đọc high, low, close
d2 = df.rename(columns={"volume": "kl"})
d2.finlens.obv(volume="kl") # frame của bạn đặt tên cột khácprice= không đi vào tên cột: rsi(14) và rsi(14, price="open") cùng
muốn tạo rsi_14, nên lần thứ hai ném FL_TA_INPUT. Đặt suffix="open" để
tách chúng ra.
Thiếu một cột thì bạn nhận lỗi có tên cột cụ thể, không phải KeyError trần:
df[["symbol", "date", "close"]].finlens.atr(14)
# ValidationError: [FL_TA_INPUT] `ATR` cần cột `high` nhưng frame không có.
# Frame này là `eod.ohlcv`. Các cột đang có: symbol, date, close.Tách theo mã — cái bẫy quan trọng nhất
Một DataFrame nhiều mã là mấy chuỗi giá nối đuôi nhau trong cùng một cột. Chỉ
báo nào cũng có một cửa sổ nhìn lùi, nên cửa sổ của mã sau ăn vào giá của mã
trước.
Đây là chỗ hỏng đáng sợ nhất trong cả trang, vì nó không có triệu chứng nào:
import talib
df = client.eod.stock.ohlcv(["HPG", "VCB", "FPT"],
start="2024-01-01", end="2025-06-30") # 1.107 dòng
dung = df.finlens.rsi(14)["rsi_14"]
sai = talib.RSI(df["close"].to_numpy(), 14)Trên 1.107 dòng đó, hai cột khác nhau ở 738 dòng — 66,7%. Con số ấy đúng
bằng toàn bộ số dòng của HPG và VCB: mọi mã trừ mã đầu bảng đều hỏng từ dòng
đầu tới dòng cuối, chứ không riêng vùng đầu chuỗi. Chỗ lệch lớn nhất là 56,27
điểm RSI. Và mọi giá trị sai đều nằm gọn trong khoảng 0–100 hợp lệ: không
exception, không cảnh báo, không một con số nào trông bất thường trên màn hình.
Dòng đầu tiên của VCB là chỗ dễ thấy nhất. df.finlens.rsi(14) cho NaN ở đó
vì chuỗi của VCB mới bắt đầu; gọi thẳng talib cho một con số — 97,98 —
dựng từ 14 phiên cuối của HPG, mã nằm ngay trên nó trong bảng.
Accessor tự dò cột khoá, và bạn tắt được nó
df.finlens.* dò cột nhóm trong chính frame — symbol với giá cổ phiếu, icb
với chỉ số ngành. Muốn chỉ rõ thì truyền by="ten_cot".
by=None tắt hẳn việc tách và cho đúng con số sai ở trên. Chỉ dùng nó khi
bạn chắc chắn frame chỉ chứa một chuỗi giá duy nhất — và trong trường hợp đó
thì bỏ trống cũng đã đúng rồi.
df.finlens.patterns() — quét 61 mẫu nến
Mẫu nến có 61 hàm, và hỏi từng cái một là 61 lời gọi cùng 61 cột thưa.
patterns() quét cả bộ và trả về dạng dài, chỉ gồm các lần bắt được.
df.finlens.patterns(
which=None,
*,
direction=None,
penetration=None,
open="open",
high="high",
low="low",
close="close",
by=...,
) -> DataFrame| Tham số | Kiểu | Mặc định | Nội dung |
|---|---|---|---|
which | str, danh sách, hoặc None | None | Tên mẫu cần quét. None là cả 61 mẫu. |
direction | "tang" · "giam" · None | None | Lọc theo chiều mẫu ngụ ý. |
penetration | float hoặc None | None | Đè lên ngưỡng xuyên của bảy mẫu có tham số này. |
open / high / low / close | str | tên chuẩn | Tên cột giá, khi frame của bạn đặt tên khác. |
by | str hoặc None | tự dò | Cột khoá để tách nhóm. |
df.finlens.patterns()symbol date pattern ten_mau signal direction
FPT 2024-01-10 CDLHIKKAKE Hikkake Pattern -200 giam
FPT 2024-01-16 CDLBELTHOLD Belt-hold 100 tang
FPT 2024-01-16 CDLCLOSINGMARUBOZU Closing Marubozu 100 tang
FPT 2024-01-16 CDLLONGLINE Long Line Candle 100 tang
FPT 2024-01-16 CDLMARUBOZU Marubozu 100 tang
FPT 2024-01-17 CDLDOJI Doji 100 tang
FPT 2024-01-17 CDLHARAMI Harami Pattern -80 giam
FPT 2024-01-17 CDLHARAMICROSS Harami Cross Pattern -80 giamCột trả về
| Cột | dtype | Có thể null | Nội dung |
|---|---|---|---|
symbol | string | không | Khoá nhóm. Tên và dtype sao chép từ frame đầu vào — là icb với chỉ số ngành, và vắng mặt khi frame chỉ có một chuỗi giá. |
date | datetime64[ns] | không | Phiên mà mẫu hoàn tất — thanh cuối của mẫu, không phải thanh đầu. Naive. |
pattern | string | không | Tên hàm TA-Lib, viết hoa: "CDLENGULFING". Đây là khoá máy đọc. |
ten_mau | string | không | Tên mẫu do TA-Lib đặt, giữ nguyên tiếng Anh: "Engulfing Pattern". |
signal | int32 | không | Cường độ có dấu. Không bao giờ bằng 0 trong bảng này. |
direction | string | không | "tang" (signal dương) hoặc "giam" (signal âm). Không dấu — đây là giá trị dữ liệu, không phải văn xuôi. |
Frame đã sắp theo (symbol, date, pattern) và mang RangeIndex. Kết quả rỗng
vẫn đủ sáu cột đúng dtype.
Với frame trong phiên, cột thời gian là time mang múi giờ
Asia/Ho_Chi_Minh, đúng như bảng giá gốc:
iv = client.intraday.stock.ohlcv(["HPG", "VCB"], interval="15min",
start="2026-08-04")
iv.finlens.patterns().head(3)symbol time pattern ten_mau signal direction
HPG 2026-08-04 10:30:00+07:00 CDLENGULFING Engulfing Pattern -80 giam
HPG 2026-08-04 11:00:00+07:00 CDLMATCHINGLOW Matching Low 100 tang
HPG 2026-08-04 13:15:00+07:00 CDLBELTHOLD Belt-hold 100 tangFrame không có cột thời gian nào thì bị từ chối, không trả bảng rỗng: một lần bắt mẫu mà không biết nó rơi vào phiên nào thì không dùng được vào việc gì.
`signal == 100` âm thầm đánh rơi một phần kết quả
Chỉ 56 trên 61 mẫu chỉ ra ±100. Năm mẫu còn lại ra giá trị khác, và chúng
không hiếm:
| Mẫu | Giá trị ngoài ±100 |
|---|---|
CDLENGULFING, CDLHARAMI, CDLHARAMICROSS | ±80 |
CDLHIKKAKE, CDLHIKKAKEMOD | ±200 |
Trên 1.107 phiên của ba mã, patterns() trả 2.184 dòng, trong đó 156
dòng có |signal| ≠ 100. Lọc bằng df[df.signal == 100] giữ lại 1.184 trên
1.265 dòng tăng giá — đánh rơi 81 dòng mà không có gì báo.
Lọc bằng dấu, hoặc bằng chính cột đã suy sẵn:
tang = pat[pat["signal"] > 0] # đúng
tang = df.finlens.patterns(direction="tang") # cũng đúngLuật này áp cho cả cột một mẫu. df.finlens.pattern.cdlengulfing() và
finlens.ta.pattern.CDLENGULFING(...) trả về một cột int32 mà 0 nghĩa là
không có mẫu, nên câu hỏi "phiên nào bắt được" là != 0, không phải == 100:
d = df.finlens.pattern.cdlengulfing()
d[d["cdlengulfing"] != 0] # 115 dòng — mọi lần bắt được, đúng
d[d["cdlengulfing"] > 0] # 37 dòng — chỉ chiều tăng, đúng
d[d["cdlengulfing"] == 100] # 13 dòng — đánh rơi 24 trong 37Và một mẫu không nhất thiết chạy được cả hai chiều: 20 trên 61 mẫu không bao
giờ ra số âm, 14 mẫu không bao giờ ra số dương. patterns("CDLDOJI", direction="giam") vì thế luôn rỗng, và bảng rỗng đó không có nghĩa là "thị
trường không giảm".
Phần Notes ở cuối docstring mỗi mẫu khai khoảng giá trị thực tế của mẫu
đó — kể cả việc nó có một chiều hay hai; phần phía trên là docstring gốc của
TA-Lib.
which= là tên mẫu, không phải chuỗi con
Ba cách viết đầu tương đương và đều trả về đúng một mẫu; truyền danh sách thì được đúng những mẫu bạn nêu, không hơn:
df.finlens.patterns("doji") # 151 dòng
df.finlens.patterns("DOJI") # 151 dòng
df.finlens.patterns("CDLDOJI") # 151 dòng
df.finlens.patterns(["CDLENGULFING", "CDLHAMMER", "CDLHIKKAKE"]) # 247 dòng"doji" không kéo theo CDLDOJISTAR hay CDLDRAGONFLYDOJI — đó là những
cái tên khác, hỏi riêng thì được 18 và 19 dòng. Tên không tồn tại nhận lỗi kèm
chỗ tra danh sách đầy đủ, chứ không phải một bảng rỗng:
df.finlens.patterns("khong_co_mau_nay")
# ValidationError: [FL_TA_PARAM] Không có mẫu nến nào tên `khong_co_mau_nay`.
# Thư viện quét đúng 61 mẫu `CDL*` của TA-Lib; xem `finlens.ta.pattern` để có
# danh sách đầy đủ.Nêu tên mẫu là chuyện đáng làm khi frame lớn: quét cả 61 mẫu cho ra khoảng hai dòng kết quả trên mỗi dòng đầu vào — 2.184 dòng từ 1.107 dòng giá. Sáu mẫu một nến bắn gần như liên tục chiếm phần lớn con số đó.
penetration=None không phải "không truyền gì cho xong"
Bảy mẫu nhận tham số penetration, và mặc định của chúng khác nhau:
| Mặc định | Mẫu |
|---|---|
0.5 | CDLDARKCLOUDCOVER, CDLMATHOLD |
0.3 | CDLABANDONEDBABY, CDLEVENINGDOJISTAR, CDLEVENINGSTAR, CDLMORNINGDOJISTAR, CDLMORNINGSTAR |
Nên penetration=None nghĩa là mỗi mẫu dùng mặc định của nó, còn một con số
nghĩa là đè lên cả bảy. Hai chuyện khác nhau, và số dòng bắt được đổi theo.
finlens.ta.* — tầng mảng
Dùng khi bạn đã có sẵn mảng. Vào mảng, ra mảng; không frame, không cột, không cảnh báo.
import finlens
close = df[df["symbol"] == "HPG"]["close"].to_numpy()
rsi = finlens.ta.RSI(close, timeperiod=14)
rsi[-3:]
# array([65.8890942 , 66.52569977, 62.44669082])Truyền Series thì nhận lại Series, giữ nguyên index và mang name:
s = finlens.ta.RSI(df[df["symbol"] == "HPG"]["close"], timeperiod=14)
s.name
# 'rsi'Hàm nhiều đầu ra trả về tuple các mảng, đúng thứ tự docstring khai:
macd, signal, hist = finlens.ta.MACD(close)Mẫu nến nằm ở finlens.ta.pattern và trả int32:
o, h, l, c = (df[df["symbol"] == "HPG"][k].to_numpy()
for k in ["open", "high", "low", "close"])
finlens.ta.pattern.CDLENGULFING(o, h, l, c)
# mảng int32, các giá trị xuất hiện: -100, -80, 0, 80, 100Tầng mảng là keyword-only tuyệt đối
Chỉ các mảng đầu vào đi theo vị trí. Một con số ở vị trí thứ hai ném
TypeError — và đó là một TypeError của Python, không phải FinLensError:
finlens.ta.RSI(close, timeperiod=14) # đúng
finlens.ta.RSI(close, 14) # TypeError: RSI() takes 1 positional
# argument but 2 were givenLuật này là hàng rào cho một lớp bug im lặng: MACD có ba số nguyên liền nhau
và STOCH có năm, nên đảo hai tham số cho ra một chỉ báo khác mà không có gì
báo.
Tầng accessor thì nới hơn một bậc: tham số đầu tiên đi được theo vị trí, các tham số sau thì không.
df.finlens.rsi(14) # chạy
df.finlens.macd(5, 35, 5) # TypeError: takes from 1 to 2 positional
# arguments but 4 were givenNhững cái phải biết trước khi tin một con số
Warm-up: bao nhiêu giá trị đầu là NaN
Số NaN đầu chuỗi đúng bằng lookback của hàm, và lookback đổi theo tham số.
RSI với timeperiod=14 bỏ đúng 14 dòng đầu của mỗi mã:
out = df.finlens.rsi(14)
out.groupby("symbol")["rsi_14"].apply(lambda s: s.isna().sum())
# symbol
# FPT 14
# HPG 14
# VCB 14macd() mặc định bỏ 33 dòng. Con số của từng hàm nằm trong help().
Mẫu nến khác chỗ này và đó là chỗ đáng nhớ: warm-up của chúng là số 0, mà
0 cũng chính là "không có mẫu". Hai chuyện đó không phân biệt được từ con số.
patterns() xoá hẳn cái bẫy đó — vùng warm-up đơn giản là không sinh dòng nào.
Chuỗi ngắn hơn lookback ra toàn NaN, không lỗi
ngan = df[df["symbol"] == "HPG"].head(10)
ngan.finlens.rsi(14)["rsi_14"].tolist()
# [nan, nan, nan, nan, nan, nan, nan, nan, nan, nan]Ở tầng accessor bạn được một DataQualityWarning nói rõ nhóm nào:
`RSI` cần ít nhất 15 dòng mỗi nhóm nhưng 1 nhóm (`symbol`) ngắn hơn thế: HPG.
Chúng ra TOÀN `NaN` — TA-Lib không báo lỗi cho trường hợp này.Ở tầng mảng không có ai đứng giữa: cùng chuỗi 10 thanh đó,
finlens.ta.pattern.CDLDOJI(...) trả về mười số 0 và im lặng.
Một ô thiếu ở giữa chuỗi làm hỏng phần còn lại
Không phải "hỏng một dòng" — hỏng tới hết chuỗi, vĩnh viễn. Đặt một NaN
vào close ở dòng thứ 31 của một chuỗi 60 dòng:
g = df[df["symbol"] == "HPG"].head(60).reset_index(drop=True)
g.loc[30, "close"] = float("nan")
r = g.finlens.rsi(14)
[round(v, 3) for v in r["rsi_14"][28:33]]
# [64.567, 72.134, nan, nan, nan]
r["rsi_14"][30:].isna().sum()
# 30 — tức toàn bộ 30 dòng còn lạiCảnh báo đi kèm chỉ ra dòng nào, và nêu một biến thể tệ hơn:
`RSI`: 1 nhóm có ô thiếu ở GIỮA chuỗi (HPG (dòng thứ 31 của nhóm)). Từ ô đó tới
cuối chuỗi kết quả thường là `NaN` — TA-Lib không tự bỏ qua ô thiếu.Vài chỉ báo nhiều cột — AD, ADOSC, MFI, PLUS_DM, MINUS_DM — so hai cột
với nhau bằng phép so sánh, mà mọi phép so sánh với NaN đều sai, nên chúng vẫn
ra số: chỉ là số khác. Thư viện không tự điền: ffill là bịa số,
dropna là đổi cửa sổ, và cả hai đều là quyết định của bạn.
Với mẫu nến thì còn tệ hơn: kết quả không thành `NaN`
Mẫu nến trả int32, nên không có NaN nào xuất hiện để bạn nghi ngờ. Mẫu
nến so thân nến với một trung bình chạy, và một ô thiếu đầu độc trung bình đó
tới tận cuối chuỗi — các mẫu vẫn ra số, chỉ là bắt sai chỗ.
Vì patterns() chỉ trả các lần bắt được, hỏng ở đây trông đúng như "phiên đó
không có mẫu nào". Cùng 369 phiên của HPG, đặt một NaN vào close ở dòng
101:
g.finlens.patterns() # 674 dòng
g_thieu.finlens.patterns() # 341 dòng — mất 333, tất cả từ dòng 101 trở điỞ tầng accessor bạn được cảnh báo:
`patterns`: 1 nhóm có ô thiếu ở GIỮA chuỗi OHLC (HPG (dòng thứ 101 của nhóm)).
Mẫu nến so thân nến với một trung bình chạy, nên một ô thiếu đầu độc trung bình
đó tới tận cuối chuỗi. ⚠️ Khác chỉ báo, kết quả **không** thành `NaN`: các mẫu
vẫn ra số, chỉ là bắt sai chỗ. Lọc bỏ hoặc vá các phiên thiếu giá trước khi quét.Ở tầng finlens.ta.pattern.* thì không có cảnh báo nào — bạn nhận về một mảng
số nguyên trông hoàn toàn bình thường.
Thứ tự dòng quyết định kết quả
Chỉ báo đọc chuỗi theo thứ tự. Frame chưa sắp tăng dần theo thời gian cho ra những con số khác hẳn — và ở tầng accessor điều đó được xử lý giúp bạn:
`RSI`: 3 nhóm (`symbol`) chưa sắp xếp tăng dần theo `date`: FPT, VCB, HPG. Đã
tính trên bản đã sắp rồi trả kết quả về đúng vị trí dòng gốc — thứ tự dòng bạn
nhận về không đổi.Thứ tự dòng bạn nhận về giữ nguyên, và giá trị khớp với bản đã sắp sẵn. Frame có
date trùng lặp cũng nhận một cảnh báo riêng, vì chỉ báo được tính trên cả dòng
thừa.
Ở tầng finlens.ta.* thì không: mảng của bạn thế nào thì TA-Lib đọc thế ấy.
24 chỉ báo đổi kết quả khi bạn đổi `start=`
Chúng có trạng thái không ổn định: cắt bớt phần đầu chuỗi làm đổi kết quả ở phần còn lại, chứ không chỉ ở vùng warm-up.
Trên 369 phiên của HPG, RSI(c)[200:] so với RSI(c[200:]) lệch tới 5,93
điểm, và phải tới phần tử thứ 96 chênh lệch mới xuống dưới 0,01. Cùng phép
đo đó, SMA lệch 7,1 × 10⁻¹⁵ — tức không lệch.
Hệ quả thực tế: lấy dữ liệu từ 2020 và lấy từ 2024 cho ra hai cột RSI khác nhau ở cùng những phiên của 2024. Không phải bug, và không có cảnh báo nào.
24 hàm đó là ADX, ADXR, ATR, CMO, DX, EMA, HT_DCPERIOD,
HT_DCPHASE, HT_PHASOR, HT_SINE, HT_TRENDLINE, HT_TRENDMODE, IMI,
KAMA, MAMA, MFI, MINUS_DI, MINUS_DM, NATR, PLUS_DI, PLUS_DM,
RSI, STOCHRSI, T3. Docstring của chúng nói rõ, và không mẫu nến nào
nằm trong danh sách.
Đơn vị và attrs
Cột chỉ báo khai đơn vị ở cùng chỗ với cột giá từ máy chủ:
df = client.eod.stock.ohlcv(["HPG", "VCB"], start="2024-01-01").finlens.rsi(14)
df.attrs["finlens"]["units"]["close"] # 'kVND'
df.attrs["finlens"]["units"]["rsi_14"] # 'pct'Đơn vị được phân giải thật chứ không chép nguyên nhãn: một chỉ báo cùng thứ nguyên với giá lấy đúng đơn vị của cột giá nó đọc.
| Cột | Đơn vị |
|---|---|
rsi_14, natr_14, willr_14 | pct |
atr_14, sma_20, macd_12_26_9 | kVND — bằng đơn vị của close |
obv | share |
ht_dcperiod | bars |
linearreg_angle_14 | degrees |
ht_trendmode | binary |
var_5_1 | kVND^2 |
cdlengulfing, cột signal của patterns() | signal |
Ngoài đơn vị, mỗi lời gọi để lại một vết ở df.attrs["finlens"]["ta"] — một
danh sách, mỗi lần gọi thêm một mục:
df.attrs["finlens"]["ta"]
# [{'columns': ['rsi_14'], 'function': 'RSI', 'params': {'timeperiod': 14},
# 'inputs': ['close'], 'by': 'symbol', 'lookback': 14}]Đây là chỗ trả lời "cột rsi_14 này tính bằng tham số nào, trên cột nào, tách
theo cột nào" sau khi bạn đã ghi frame ra file rồi mở lại vào tuần sau.
Frame bạn tự dựng không bị dán siêu dữ liệu
Chỉ báo chạy bình thường trên một DataFrame bạn tự tạo, nhưng frame đó không
có attrs["finlens"]:
t = pd.DataFrame({"close": close}) # mảng giá bạn tự dựng
t.finlens.rsi(14).attrs
# {}Thư viện không gắn siêu dữ liệu nói dối về xuất xứ lên một bảng không phải của
nó. Và nhắc lại luật chung: df.attrs không sống sót qua pd.concat hay
merge — đọc đơn vị trước khi ghép.
Lỗi hay gặp
Bốn mã lỗi, và chúng chia làm hai nhóm theo ai sửa được:
| Mã | Lớp ngoại lệ | Nghĩa |
|---|---|---|
FL_TA_PARAM | ValidationError | Bạn gõ sai một tham số. |
FL_TA_INPUT | ValidationError | Frame này không phải thứ chỉ báo đó cần. |
FL_TA | IndicatorError | TA-Lib từ chối tính. |
FL_TA_UNAVAILABLE | IndicatorError | Không nạp được TA-Lib trên máy này. |
IndicatorError là lớp mới, con trực tiếp của FinLensError, nên
except finlens.FinLensError vẫn bắt trọn. Hai mã đầu là ValidationError — mà
ValidationError kế thừa cả ValueError, nên except ValueError sẵn có của bạn
cũng bắt được chúng.
Ba lỗi hay gặp nhất, nguyên văn:
finlens.ta.RSI(close, timeperiod=14.5)
# ValidationError: [FL_TA_PARAM] `RSI`: tham số `timeperiod` phải là số nguyên,
# nhận 14.5. TA-Lib cắt phần thập phân trong im lặng, nên `timeperiod=14.5` sẽ
# chạy như `timeperiod=14` mà không báo gì.
finlens.ta.ATR(h[:100], l, c)
# ValidationError: [FL_TA_INPUT] `ATR`: các chuỗi đầu vào lệch độ dài
# ([100, 369]). Chúng phải cùng độ dài vì mỗi vị trí là một thanh giá.
finlens.ta.RSI(close, timeperiod=1)
# IndicatorError: [FL_TA] `RSI` lỗi bên trong TA-Lib: TA_RSI function failed
# with error code 2: Bad Parameter (TA_BAD_PARAM)Mỗi thông báo còn kết thúc bằng -> https://docs.finlens.vn/python-sdk/errors/<mã>
— các đoạn trên cắt phần đuôi đó cho gọn.
FL_TA_UNAVAILABLE chỉ xuất hiện khi phần native của TA-Lib không nạp được —
thông báo của nó kèm sẵn lệnh chữa, pip install --force-reinstall ta-lib. Xem
thêm Cài đặt và Xử lý lỗi.
Bảng tra: 135 hàm, 8 nhóm
| Nhóm | Số hàm | Vài tên tiêu biểu |
|---|---|---|
| Nhận dạng mẫu nến | 61 | CDLENGULFING · CDLDOJI · CDLHAMMER · CDL3WHITESOLDIERS |
| Chỉ báo động lượng | 31 | RSI · MACD · STOCH · ADX · CCI · WILLR |
| Đường bao và trung bình | 18 | SMA · EMA · BBANDS · SAR · KAMA · T3 |
| Thống kê | 9 | STDDEV · VAR · BETA · CORREL · LINEARREG |
| Biến đổi giá | 5 | AVGPRICE · MEDPRICE · TYPPRICE · WCLPRICE |
| Chu kỳ | 5 | HT_DCPERIOD · HT_SINE · HT_TRENDMODE |
| Khối lượng | 3 | AD · ADOSC · OBV |
| Biến động | 3 | ATR · NATR · TRANGE |
Hai nhóm Math Operators và Math Transform của TA-Lib không có mặt — đó
là ADD, SIN, SQRT, thứ numpy đã làm tốt hơn.
Danh sách đầy đủ không nằm trên trang này, vì nó tra được bằng một dòng:
[n for n in finlens.ta.__all__ if n.isupper()] # 74 chỉ báo
[n for n in finlens.ta.pattern.__all__ if n.isupper()] # 61 mẫu nếnVà mỗi hàm tự mô tả mình. help() cho bạn chữ ký, tham số, giá trị mặc định,
đơn vị, khoảng giá trị, warm-up, cùng những cái bẫy của riêng hàm đó:
help(finlens.ta.RSI)RSI(real: 'Real', *, timeperiod: 'int' = 14) -> 'Real'
Relative Strength Index — nhóm chỉ báo động lượng.
...
Notes
-----
Đơn vị của kết quả: phần trăm. Khoảng giá trị: 0..100.
**Warm-up: 14 giá trị `NaN` đầu tiên.** Đổi tham số thì con số này đổi theo.
...Đây là chỗ tra đáng tin nhất khi bạn phân vân về một hàm cụ thể — nó đi kèm
đúng bản finlens bạn đang chạy.
Xem tiếp
Dữ liệu cuối ngày (EOD)
Nguồn của bảng giá bạn đưa vào chỉ báo: ohlcv() cho cổ phiếu, chỉ số,
phái sinh, chứng quyền và chỉ số ngành ICB.
Dữ liệu trong phiên
Thanh giá theo phút để quét mẫu nến trong ngày. Cột thời gian là time và
mang múi giờ.
Kiểu dữ liệu
Toàn bộ nội dung của df.attrs["finlens"], gồm units và vết ta.
Xử lý lỗi
IndicatorError, bốn mã FL_TA*, và cây ngoại lệ đầy đủ.
Câu hỏi thường gặp
Làm sao tính RSI cho nhiều mã cổ phiếu cùng lúc bằng Python?
Dùng accessor `df.finlens.rsi(14)` của thư viện Python finlens ngay trên `DataFrame` giá bạn vừa lấy về. Nó tự tách theo cột `symbol` rồi tính riêng từng mã, nên cửa sổ 14 phiên của mã sau không bao giờ ăn vào giá của mã trước. Kết quả là một bản sao của frame kèm cột `rsi_14`; frame gốc không bị đụng tới. Nối chuỗi được: `df.finlens.rsi(14).finlens.macd()` cho ra cả bốn cột trong một biểu thức.
Vì sao gọi thẳng talib.RSI trên DataFrame nhiều mã lại ra số sai?
Vì một `DataFrame` nhiều mã là mấy chuỗi giá nối đuôi nhau trong cùng một cột, và TA-Lib không biết cột `symbol` tồn tại. Cửa sổ nhìn lùi của mã sau vì thế ăn vào giá cuối của mã trước. Đo trên ba mã và 1.107 phiên bằng thư viện Python finlens: hai cột khác nhau ở 738 dòng, tức 66,7% — đúng bằng mọi dòng của hai mã đứng sau, hỏng từ đầu tới cuối. Chỗ lệch lớn nhất là 56,27 điểm RSI, mà mọi giá trị sai vẫn nằm trong khoảng 0–100 hợp lệ nên không có gì trên màn hình để nghi ngờ.
Tên cột mà df.finlens.rsi(14) tạo ra là gì?
Là `rsi_14` — tên cột được dựng từ tên đầu ra cộng các tham số. Trong thư viện Python finlens, `rsi(21)` cho `rsi_21`, `sma(20)` rồi `sma(50)` cho hai cột `sma_20` và `sma_50` chứ không đè lên nhau, `macd()` cho `macd_12_26_9`, `macdsignal_12_26_9`, `macdhist_12_26_9`, còn hàm không tham số như `obv()` thì cột chỉ là `obv`. Gọi lại đúng một chỉ báo với đúng tham số cũ ném `ValidationError` mã `FL_TA_INPUT`; đặt `suffix=` để có tên khác.
Quét mẫu nến trên dữ liệu chứng khoán Việt Nam bằng Python thế nào?
Gọi `df.finlens.patterns()` của thư viện Python finlens trên một frame có đủ `open`, `high`, `low`, `close`. Nó quét cả 61 mẫu nến của TA-Lib và trả về dạng dài, chỉ gồm các lần bắt được, với sáu cột `symbol`, `date`, `pattern`, `ten_mau`, `signal`, `direction`. Nêu tên mẫu để thu hẹp: `patterns("doji")` chỉ quét `CDLDOJI` chứ không kéo theo `CDLDOJISTAR`. Frame trong phiên cho cột `time` mang múi giờ thay cho `date`.
Vì sao lọc mẫu nến bằng signal == 100 lại thiếu kết quả?
Vì chỉ 56 trên 61 mẫu nến chỉ ra ±100. `CDLENGULFING`, `CDLHARAMI` và `CDLHARAMICROSS` còn ra ±80, còn `CDLHIKKAKE` và `CDLHIKKAKEMOD` ra ±200 cho thanh xác nhận. Đo bằng thư viện Python finlens trên ba mã và 1.107 phiên: trong 2.184 dòng kết quả có 156 dòng mang giá trị ngoài ±100, và lọc bằng `signal == 100` đánh rơi 81 trên 1.265 dòng tăng giá. Lọc bằng `df.signal > 0`, hoặc bằng cột `direction` đã suy sẵn từ dấu.
Cắt bớt dữ liệu đầu chuỗi có làm đổi giá trị RSI không?
Có. RSI thuộc nhóm 24 chỉ báo có trạng thái không ổn định, nghĩa là bỏ bớt phần đầu chuỗi làm đổi kết quả ở cả phần còn lại chứ không chỉ ở vùng warm-up. Đo bằng thư viện Python finlens trên 369 phiên của HPG: `RSI(c)[200:]` lệch `RSI(c[200:])` tới 5,93 điểm, và phải tới phần tử thứ 96 chênh lệch mới xuống dưới 0,01. Hệ quả thực tế là lấy dữ liệu từ 2020 và từ 2024 cho hai cột RSI khác nhau ở cùng những phiên. `SMA` thì không bị.
finlens.ta.RSI(close, 14) báo TypeError, phải viết thế nào?
Viết `finlens.ta.RSI(close, timeperiod=14)`. Ở tầng mảng của thư viện Python finlens, chỉ các mảng đầu vào đi theo vị trí, mọi tham số còn lại là keyword-only. Đây là hàng rào cho một lớp bug im lặng: `MACD` có ba số nguyên liền nhau và `STOCH` có năm, nên đảo hai tham số cho ra một chỉ báo khác mà không báo gì. Tầng accessor nới hơn một bậc — `df.finlens.rsi(14)` chạy được, nhưng `df.finlens.macd(5, 35, 5)` vẫn ném `TypeError`.
Cập nhật lần cuối