Trang chủBlogChiến Lược Nâng CaoLọc cổ phiếu mạnh hơn VN-Index bằng dữ liệu 20 phiên và 60 phiên
Lọc cổ phiếu mạnh hơn VN-Index bằng dữ liệu 20 phiên và 60 phiên
Chiến Lược Nâng Cao

Lọc cổ phiếu mạnh hơn VN-Index bằng dữ liệu 20 phiên và 60 phiên

Lọc cổ phiếu

Ngọc Hải LươngNgọc Hải Lương
21/7/2026
11 phút phút đọc
49 lượt xem

Khi tìm cổ phiếu để theo dõi, nhiều người thường bắt đầu bằng việc xem mã nào đang tăng giá mạnh nhất. Cách nhìn này khá trực quan nhưng chưa đủ, bởi một cổ phiếu tăng 5% trong lúc VN-Index tăng 10% thực chất đang yếu hơn thị trường. Ngược lại, một cổ phiếu giảm 2% trong giai đoạn VN-Index giảm 8% vẫn có thể được xem là đang thể hiện sức mạnh tương đối tốt.

Vì vậy, thay vì chỉ hỏi “cổ phiếu này tăng bao nhiêu?”, chúng ta có thể đặt câu hỏi rõ hơn: trong cùng một khoảng thời gian, cổ phiếu này tốt hơn hay kém hơn VN-Index bao nhiêu phần trăm?

Bài viết sử dụng dữ liệu 20 phiên và 60 phiên để xây một bộ lọc đơn giản bằng Python. Mục tiêu không phải tạo ra tín hiệu mua tự động, mà là thu hẹp danh sách cổ phiếu xuống những mã đang được dòng tiền ưu tiên hơn thị trường chung.

c4d35138-6c74-47d6-921b-cf9ce818361b.png

Phần 1. “Mạnh hơn VN-Index” thực chất được tính như thế nào?

Trong bài này, sức mạnh tương đối được hiểu là chênh lệch giữa lợi suất của cổ phiếu và lợi suất của VN-Index trong cùng một giai đoạn.

Công thức khá đơn giản:

Sức mạnh tương đối 20 phiên = Lợi suất cổ phiếu 20 phiên − Lợi suất VN-Index 20 phiên

Tương tự:

Sức mạnh tương đối 60 phiên = Lợi suất cổ phiếu 60 phiên − Lợi suất VN-Index 60 phiên

Giả sử trong 20 phiên gần nhất, FPT tăng 12% còn VN-Index tăng 5%. Khi đó:

RS20 của FPT = 12% − 5% = 7%

Điều này có nghĩa FPT đang vượt VN-Index khoảng 7 điểm phần trăm trong cùng giai đoạn.

Một trường hợp khác, cổ phiếu giảm 3% nhưng VN-Index giảm 8%. Khi đó:

RS20 = −3% − (−8%) = 5%

Cổ phiếu vẫn có sức mạnh tương đối dương, dù bản thân giá đang giảm. Nó không phải một cổ phiếu tăng giá, nhưng đang giảm ít hơn thị trường chung.

Đây là điểm cần phân biệt rõ: mạnh hơn thị trường không đồng nghĩa với đang trong xu hướng tăng. Một mã có thể mạnh tương đối chỉ vì nó giảm chậm hơn VN-Index. Vì vậy, khi lọc cổ phiếu, nên quan sát đồng thời cả lợi suất tuyệt đối và sức mạnh tương đối.

Việc sử dụng hai khung 20 phiên và 60 phiên giúp nhìn được hai lớp thông tin khác nhau.

Dữ liệu 20 phiên, tương đương khoảng một tháng giao dịch, phản ánh sức mạnh ngắn hạn. Nó cho biết trong thời gian gần đây, dòng tiền có đang chuyển sang cổ phiếu hay không.

Dữ liệu 60 phiên, tương đương khoảng ba tháng giao dịch, phản ánh xu hướng trung hạn. Nó giúp phân biệt một mã mới tăng trong vài phiên với một mã đã duy trì sức mạnh trong thời gian dài hơn.

Từ hai chỉ số này, cổ phiếu có thể được chia thành bốn nhóm.

Screenshot 2026-07-21 232832.png

Nhóm đầu tiên thường là nơi đáng quan sát nhất. Đây là những cổ phiếu không chỉ vừa tăng trong vài phiên, mà đã duy trì khả năng vượt thị trường trong cả khoảng thời gian dài hơn.

Nhóm “đang cải thiện” cũng khá đáng chú ý. Một cổ phiếu có RS60 âm nhưng RS20 chuyển sang dương có thể đang ở giai đoạn đầu của quá trình phục hồi. Tuy nhiên, tín hiệu này chưa đủ mạnh vì cổ phiếu vẫn còn yếu nếu nhìn trong ba tháng.

Ngược lại, nhóm “đang suy yếu” thường gồm những mã từng dẫn dắt nhưng gần đây bắt đầu mất động lượng. Đây chưa chắc là tín hiệu bán, nhưng có thể là dấu hiệu dòng tiền đang dịch chuyển sang nhóm khác.

Phần 2. Lấy dữ liệu VN-Index và cổ phiếu bằng Vnstock

Ví dụ dưới đây sử dụng giao diện hợp nhất của Vnstock phiên bản 4 trở lên. Theo tài liệu chính thức, dữ liệu OHLCV của cổ phiếu có thể lấy qua market.equity.ohlcv(), còn dữ liệu chỉ số được lấy qua market.index.ohlcv(). Kết quả trả về dưới dạng DataFrame để tiếp tục xử lý bằng Pandas.

Trước tiên, cài đặt các thư viện cần thiết: %pip install -q -U vnstock pandas numpy matplotlib

Sau đó khai báo thư viện và khoảng thời gian lấy dữ liệu:

import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from vnstock import Market

market = Market()

END_DATE = pd.Timestamp.today().strftime("%Y-%m-%d")
START_DATE = (
    pd.Timestamp.today() - pd.Timedelta(days=400)
).strftime("%Y-%m-%d")

print("Từ ngày:", START_DATE)
print("Đến ngày:", END_DATE)

Khoảng 400 ngày theo lịch được sử dụng để bảo đảm có đủ hơn 60 phiên giao dịch sau khi loại bỏ ngày nghỉ, cuối tuần và các quan sát thiếu.

Trong ví dụ này, chúng ta dùng một danh sách mã mẫu. Có thể thay thế bằng danh mục theo dõi cá nhân hoặc một rổ cổ phiếu có thanh khoản tốt.

SYMBOLS = [ "ACB", "BID", "CTG", "FPT", "GAS", "GMD", "HPG", "MBB", "MWG", "PNJ", "SSI", "STB", "TCB", "VCB", "VHM", "VIC", "VNM", "VRE" ]

Tiếp theo, viết một hàm để chuẩn hóa dữ liệu. Bước này giúp bảo đảm cột thời gian được chuyển đúng định dạng, giá và khối lượng là dữ liệu số, đồng thời loại các ngày bị trùng hoặc thiếu giá.

def clean_ohlcv(data: pd.DataFrame) -> pd.DataFrame:

"""

Chuẩn hóa dữ liệu OHLCV trả về từ Vnstock.

"""

if data is None or data.empty:

raise ValueError("Không có dữ liệu trả về.")

data = data.copy()

required_columns = {

"time", "open", "high",

"low", "close", "volume"

}

missing_columns = required_columns - set(data.columns)

if missing_columns:

raise ValueError(

f"Dữ liệu thiếu cột: {sorted(missing_columns)}"

)

data["time"] = pd.to_datetime(

data["time"],

errors="coerce"

)

numeric_columns = [

"open", "high", "low", "close", "volume"

]

for column in numeric_columns:

data[column] = pd.to_numeric(

data[column],

errors="coerce"

)

data = (

data.dropna(subset=["time", "close"])

.sort_values("time")

.drop_duplicates(subset="time", keep="last")

.set_index("time")

)

data = data[

(data["close"] > 0)

& (data["volume"] >= 0)

].copy()

return data

Bây giờ lấy dữ liệu VN-Index:

vnindex_raw = market.index.ohlcv(
    symbol="VNINDEX",
    start=START_DATE,
    end=END_DATE
)

vnindex = clean_ohlcv(vnindex_raw)

print(vnindex.tail())
Vnstock hỗ trợ dữ liệu giá lịch sử cho cả cổ phiếu và chỉ số thị trường; tài liệu của dự án cũng liệt kê nhóm dữ liệu index riêng bên cạnh nhóm equity.

Tiếp theo, viết hàm tính sức mạnh tương đối của từng cổ phiếu so với VN-Index.

Điểm quan trọng là dữ liệu cổ phiếu và chỉ số phải được ghép theo đúng ngày giao dịch. Không nên lấy dòng thứ 20 của cổ phiếu trừ dòng thứ 20 của chỉ số khi hai chuỗi có ngày bị thiếu khác nhau

def calculate_relative_strength(

symbol: str,

index_data: pd.DataFrame

) -> dict:

"""

Tính lợi suất và sức mạnh tương đối

20 phiên, 60 phiên của một cổ phiếu.

"""

stock_raw = market.equity.ohlcv(

symbol=symbol,

start=START_DATE,

end=END_DATE

)

stock = clean_ohlcv(stock_raw)

stock = stock.rename(columns={

"close": "stock_close",

"volume": "stock_volume"

})

index_close = index_data[["close"]].rename(

columns={"close": "index_close"}

)

merged = stock[

["stock_close", "stock_volume"]

].join(

index_close,

how="inner"

).dropna()

if len(merged) < 61:

raise ValueError(

f"{symbol} không đủ 60 phiên dữ liệu."

)

stock_return_20 = (

merged["stock_close"].iloc[-1]

/ merged["stock_close"].iloc[-21]

- 1

)

index_return_20 = (

merged["index_close"].iloc[-1]

/ merged["index_close"].iloc[-21]

- 1

)

stock_return_60 = (

merged["stock_close"].iloc[-1]

/ merged["stock_close"].iloc[-61]

- 1

)

index_return_60 = (

merged["index_close"].iloc[-1]

/ merged["index_close"].iloc[-61]

- 1

)

rs20 = stock_return_20 - index_return_20

rs60 = stock_return_60 - index_return_60

# Giá trị giao dịch bình quân 20 phiên

merged["trading_value"] = (

merged["stock_close"]

* merged["stock_volume"]

)

average_value_20 = (

merged["trading_value"]

.tail(20)

.mean()

)

# Kiểm tra xu hướng đơn giản

ma20 = merged["stock_close"].tail(20).mean()

ma60 = merged["stock_close"].tail(60).mean()

current_price = merged["stock_close"].iloc[-1]

return {

"symbol": symbol,

"date": merged.index[-1],

"price": current_price,

"stock_return_20": stock_return_20,

"vnindex_return_20": index_return_20,

"rs20": rs20,

"stock_return_60": stock_return_60,

"vnindex_return_60": index_return_60,

"rs60": rs60,

"avg_value_20_bn": average_value_20 / 1_000_000_000,

"above_ma20": current_price > ma20,

"above_ma60": current_price > ma60

}

Sau đó chạy lần lượt cho danh sách cổ phiếu:

results = []

errors = []

for symbol in SYMBOLS:

try:

result = calculate_relative_strength(

symbol=symbol,

index_data=vnindex

)

results.append(result)

print(f"Đã xử lý: {symbol}")

# Nghỉ ngắn giữa các lần gọi dữ liệu

time.sleep(0.5)

except Exception as error:

errors.append({

"symbol": symbol,

"error": str(error)

})

print(f"Lỗi {symbol}: {error}")

Chuyển kết quả thành bảng:

screen = pd.DataFrame(results)

percentage_columns = [

"stock_return_20",

"vnindex_return_20",

"rs20",

"stock_return_60",

"vnindex_return_60",

"rs60"

]

for column in percentage_columns:

screen[column] = screen[column] * 100

screen = screen.sort_values(

by=["rs20", "rs60"],

ascending=False

)

display_columns = [

"symbol",

"date",

"price",

"stock_return_20",

"rs20",

"stock_return_60",

"rs60",

"avg_value_20_bn",

"above_ma20",

"above_ma60"

]

print(

screen[display_columns]

.round(2)

.to_string(index=False)

)

Phần 3. Xây bộ lọc thực tế thay vì chỉ xếp hạng mã tăng mạnh nhất

Sau khi có RS20 và RS60, cách đơn giản nhất là chọn những mã có cả hai chỉ số đều dương:

leaders = screen[
    (screen["rs20"] > 0)
    & (screen["rs60"] > 0)
].copy()

leaders = leaders.sort_values(
    by=["rs20", "rs60"],
    ascending=False
)

print(
    leaders[
        [
            "symbol",
            "stock_return_20",
            "rs20",
            "stock_return_60",
            "rs60"
        ]
    ]
    .round(2)
    .to_string(index=False)
)

Tuy nhiên, bộ lọc này vẫn có thể chọn một cổ phiếu đang giảm. Chẳng hạn cổ phiếu giảm 3% trong khi VN-Index giảm 10%, RS vẫn dương 7%.

Để tìm những mã vừa mạnh hơn thị trường, vừa có lợi suất tuyệt đối tích cực, có thể bổ sung điều kiện:

leaders_positive = screen[
    (screen["rs20"] > 0)
    & (screen["rs60"] > 0)
    & (screen["stock_return_20"] > 0)
    & (screen["stock_return_60"] > 0)
].copy()

Đây là bộ lọc chặt hơn. Cổ phiếu phải đáp ứng đồng thời:


  • Tăng giá trong 20 phiên;


  • Tăng giá trong 60 phiên;


  • Tốt hơn VN-Index trong 20 phiên;


  • Tốt hơn VN-Index trong 60 phiên.

Tiếp theo, nên thêm điều kiện thanh khoản. Một cổ phiếu mạnh nhưng chỉ giao dịch vài trăm triệu đồng mỗi ngày có thể khó mua, khó bán và dễ bị ảnh hưởng bởi một lượng lệnh nhỏ.

Ví dụ, chỉ giữ những mã có giá trị giao dịch bình quân 20 phiên từ 20 tỷ đồng mỗi ngày:

MIN_AVG_VALUE_BN = 20

leaders_liquid = screen[
    (screen["rs20"] > 0)
    & (screen["rs60"] > 0)
    & (screen["stock_return_20"] > 0)
    & (screen["stock_return_60"] > 0)
    & (screen["avg_value_20_bn"] >= MIN_AVG_VALUE_BN)
].copy()

Có thể bổ sung điều kiện giá nằm trên MA20 và MA60:

final_screen = screen[
    (screen["rs20"] > 0)
    & (screen["rs60"] > 0)
    & (screen["stock_return_20"] > 0)
    & (screen["stock_return_60"] > 0)
    & (screen["avg_value_20_bn"] >= MIN_AVG_VALUE_BN)
    & (screen["above_ma20"])
    & (screen["above_ma60"])
].copy()

Bộ lọc này không còn chỉ tìm cổ phiếu “giảm ít hơn thị trường”. Nó hướng đến những mã có xu hướng giá tích cực, duy trì sức mạnh trên cả hai khung và có thanh khoản tương đối đủ để giao dịch.

Để xếp hạng các mã trong danh sách cuối cùng, không nên cộng trực tiếp RS20 và RS60 theo kiểu tùy ý, vì hai chỉ số có độ phân tán khác nhau. Một cách dễ hiểu hơn là chuyển chúng thành thứ hạng phần trăm.

screen["rs20_rank"] = screen["rs20"].rank(
    pct=True
)

screen["rs60_rank"] = screen["rs60"].rank(
    pct=True
)

screen["relative_strength_score"] = (
    0.5 * screen["rs20_rank"]
    + 0.5 * screen["rs60_rank"]
)

Điểm gần 1 cho biết cổ phiếu nằm trong nhóm mạnh nhất của danh sách trên cả hai khung thời gian.

final_screen = screen[
    (screen["rs20"] > 0)
    & (screen["rs60"] > 0)
    & (screen["stock_return_20"] > 0)
    & (screen["stock_return_60"] > 0)
    & (screen["avg_value_20_bn"] >= MIN_AVG_VALUE_BN)
    & (screen["above_ma20"])
    & (screen["above_ma60"])
].sort_values(
    by="relative_strength_score",
    ascending=False
)

output_columns = [
    "symbol",
    "price",
    "stock_return_20",
    "rs20",
    "stock_return_60",
    "rs60",
    "avg_value_20_bn",
    "relative_strength_score"
]

print(
    final_screen[output_columns]
    .round(2)
    .to_string(index=False)
)

Một biểu đồ scatter sẽ giúp nhìn nhanh vị trí của từng cổ phiếu:


  • Trục ngang là RS60;


  • Trục dọc là RS20;


  • Góc trên bên phải là nhóm mạnh trên cả hai khung;


  • Góc dưới bên trái là nhóm yếu hơn thị trường.

fig, ax = plt.subplots(figsize=(11, 7))

ax.scatter(
    screen["rs60"],
    screen["rs20"],
    s=80,
    alpha=0.7
)

for _, row in screen.iterrows():
    ax.annotate(
        row["symbol"],
        (row["rs60"], row["rs20"]),
        xytext=(4, 4),
        textcoords="offset points",
        fontsize=9
    )

ax.axhline(
    y=0,
    linewidth=1
)

ax.axvline(
    x=0,
    linewidth=1
)

ax.set_title(
    "Sức mạnh tương đối của cổ phiếu so với VN-Index"
)

ax.set_xlabel(
    "Sức mạnh tương đối 60 phiên (%)"
)

ax.set_ylabel(
    "Sức mạnh tương đối 20 phiên (%)"
)

ax.grid(alpha=0.3)

plt.show()

Biểu đồ này thường dễ đọc hơn một bảng dài. Những mã nằm xa về phía trên bên phải đang thể hiện sức mạnh tốt trên cả ngắn hạn và trung hạn.

Tuy nhiên, không nên coi kết quả lọc là lệnh mua. Một cổ phiếu có thể mạnh hơn VN-Index vì vừa tăng nóng, đang ở vùng định giá cao hoặc đã phản ánh phần lớn thông tin tích cực. Bộ lọc sức mạnh tương đối chỉ trả lời một câu hỏi: dòng tiền gần đây đang ưu tiên cổ phiếu nào hơn so với thị trường chung?

Trước khi quyết định giao dịch, vẫn cần kiểm tra thêm cấu trúc xu hướng, vùng hỗ trợ, thanh khoản, kết quả kinh doanh, mức định giá và rủi ro riêng của doanh nghiệp.

Một quy trình có thể thực hiện như sau:

Lọc RS20 và RS60 dương → loại cổ phiếu thanh khoản thấp → kiểm tra xu hướng giá → đọc lại thông tin doanh nghiệp → xác định điểm vào và mức rủi ro.

Cũng cần lưu ý rằng dữ liệu từ công cụ công khai có thể thiếu, gián đoạn hoặc sai lệch so với nguồn gốc. Chính dự án Vnstock khuyến nghị người dùng kiểm tra dữ liệu và hiểu rõ giới hạn trước khi sử dụng cho giao dịch hoặc ra quyết định tài chính thực tế.

Giá trị lớn nhất của bộ lọc này không nằm ở việc dự báo chắc chắn cổ phiếu nào sẽ tăng tiếp. Nó giúp chúng ta không phải xem hàng trăm biểu đồ một cách ngẫu nhiên, mà tập trung trước vào những mã đang cho thấy sức mạnh rõ hơn thị trường trên cả 20 và 60 phiên.