Logo FinLensFinLens Docs

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.

hai tầng, và bạn gần như luôn muốn tầng thứ hai.

GọiTrả 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.158631

Khô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ề

  1. 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ột symbol. Tầng finlens.ta.* thì không biết gì về mã.
  2. NaN ở đầu chuỗi không phải lỗi, đó là warm-up. Chuỗi ngắn hơn warm-up cho ra toàn NaN — 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ì".
  3. 24 trên 74 chỉ báo đổi kết quả khi bạn đổi start=, chứ không chỉ ở vùng warm-up. RSI nằm trong số đó, SMA thì không.

Hai tầng, và khi nào dùng cái nào

df.finlens.*finlens.ta.*
VàoDataFramemảng numpy hoặc Series
Rabản sao của frame, kèm cột mớimảng, hoặc tuple mảng
Bề mặt73 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 / icbkhông biết mã là gì
Cảnh báo dữ liệukhô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: RSIrsi, HT_DCPERIODht_dcperiod, MINUS_DIminus_di, CDLENGULFINGpattern.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ểuMặc địnhNội dung
Tham số của TA-Libtheo từng hàmtheo từng hàmtimeperiod, fastperiod, matype… — giữ nguyên tên gốc.
pricestr"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=.
bystr hoặc Nonetự dòCột khoá để tách nhóm. None tắt hẳn việc tách.
suffixstr hoặc NoneNoneĐ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ọiCột sinh ra
rsi(14)rsi_14
rsi(21)rsi_21
sma(20) rồi sma(50)sma_20sma_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 — obvcdlengulfing đứ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ác

price= không đi vào tên cột: rsi(14)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 HPGVCB: 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ểuMặc địnhNội dung
whichstr, danh sách, hoặc NoneNoneTên mẫu cần quét. None là cả 61 mẫu.
direction"tang" · "giam" · NoneNoneLọc theo chiều mẫu ngụ ý.
penetrationfloat hoặc NoneNoneĐè lên ngưỡng xuyên của bảy mẫu có tham số này.
open / high / low / closestrtên chuẩnTên cột giá, khi frame của bạn đặt tên khác.
bystr hoặc Nonetự 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      giam

Cột trả về

CộtdtypeCó thể nullNội dung
symbolstringkhôngKhoá 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á.
datedatetime64[ns]khôngPhiên mà mẫu hoàn tất — thanh cuối của mẫu, không phải thanh đầu. Naive.
patternstringkhôngTên hàm TA-Lib, viết hoa: "CDLENGULFING". Đây là khoá máy đọc.
ten_maustringkhôngTên mẫu do TA-Lib đặt, giữ nguyên tiếng Anh: "Engulfing Pattern".
signalint32khôngCường độ có dấu. Không bao giờ bằng 0 trong bảng này.
directionstringkhô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      tang

Frame 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ẫuGiá 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|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 đúng

Luật này áp cho cả cột một mẫu. df.finlens.pattern.cdlengulfing()finlens.ta.pattern.CDLENGULFING(...) trả về một cột int320 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 37

Và 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 địnhMẫu
0.5CDLDARKCLOUDCOVER, CDLMATHOLD
0.3CDLABANDONEDBABY, 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, 100

Tầ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 given

Luậ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 given

Nhữ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    14

macd() 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ại

Cả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ỗ.

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_14pct
atr_14, sma_20, macd_12_26_9kVND — bằng đơn vị của close
obvshare
ht_dcperiodbars
linearreg_angle_14degrees
ht_trendmodebinary
var_5_1kVND^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:

Lớp ngoại lệNghĩa
FL_TA_PARAMValidationErrorBạn gõ sai một tham số.
FL_TA_INPUTValidationErrorFrame này không phải thứ chỉ báo đó cần.
FL_TAIndicatorErrorTA-Lib từ chối tính.
FL_TA_UNAVAILABLEIndicatorErrorKhô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 đặtXử lý lỗi.

Bảng tra: 135 hàm, 8 nhóm

NhómSố hàmVài tên tiêu biểu
Nhận dạng mẫu nến61CDLENGULFING · CDLDOJI · CDLHAMMER · CDL3WHITESOLDIERS
Chỉ báo động lượng31RSI · MACD · STOCH · ADX · CCI · WILLR
Đường bao và trung bình18SMA · EMA · BBANDS · SAR · KAMA · T3
Thống kê9STDDEV · VAR · BETA · CORREL · LINEARREG
Biến đổi giá5AVGPRICE · MEDPRICE · TYPPRICE · WCLPRICE
Chu kỳ5HT_DCPERIOD · HT_SINE · HT_TRENDMODE
Khối lượng3AD · ADOSC · OBV
Biến động3ATR · NATR · TRANGE

Hai nhóm Math OperatorsMath 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ến

Và 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

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

Nội dung trang