Phiên cuối tháng và 3 phiên đầu tháng có phải "mùa vàng" của VN30?
Duy Nguyen · · Nghiên cứu thị trường
> Tóm tắt nhanh: Trên VN30F1M, 4 phiên quanh lúc sang tháng lãi trung bình +0,182%/phiên, còn các phiên khác chỉ +0,032%. Nhưng chênh lệch này đứng sát ranh giới may rủi (p=0,056), và từ 2023 đến 2026 gần như không còn thấy. Hiệu ứng có thật ở mức gió thuận nhẹ, chưa đủ để thành máy in tiền.
Anh em trên diễn đàn hay nói "đầu tháng lương về, thị trường có tiền". Câu này nghe xuôi tai đến mức ít ai chịu bỏ công kiểm tra.
Bên Mỹ thì người ta kiểm tra từ lâu rồi. Giới nghiên cứu ghi nhận chỉ số chứng khoán hay tăng tốt vào phiên cuối tháng và vài phiên đầu tháng mới, đến mức phần lớn mức tăng của cả tháng dồn vào mấy phiên đó. Hiện tượng này được gọi là hiệu ứng sang tháng.
Mình muốn biết điều đó có đúng với VN30 không. Mình kiểm tra trên ba chuỗi: hợp đồng phái sinh VN30F1M, chỉ số VN30 và một rổ chia đều các mã VN30. Kèm theo là câu hỏi mà ai cầm tiền cũng quan tâm: trừ phí xong thì còn lại bao nhiêu.
> Góc người mới (biết rồi thì lướt qua) > - Hiệu ứng sang tháng (TOM): phiên cuối tháng và vài phiên đầu tháng mới tăng tốt hơn các phiên khác, giống quán cơm gần công ty đông khách hẳn mấy ngày mới lĩnh lương. > - VN30F1M: hợp đồng tương lai trên chỉ số VN30, kỳ đáo hạn gần nhất. Bạn không sở hữu cổ phiếu, chỉ ăn chênh lệch; long là cược tăng, short là cược giảm. > - Điểm phái sinh: giá VN30F1M tính bằng điểm, mỗi điểm lên xuống là 100.000 đồng lãi/lỗ cho 1 hợp đồng. > - Mốc so sánh: kết quả của "ngày bình thường". Con được 8 điểm chỉ đáng khen khi biết điểm trung bình cả lớp là 6 chứ không phải 9. > - Khoảng tin cậy 95% / thanh lỗi: vùng mà con số thật nhiều khả năng nằm trong đó, giống dự báo "mai 28-32 độ". Thanh lỗi cắt qua 0 tức là chưa chắc khác 0. > - t-stat: độ to của tín hiệu so với tiếng ồn, như nghe người nói trong quán nhậu. Trị tuyệt đối từ khoảng 2 trở lên thì khó đổ cho may.
Vì sao đầu tháng có thể "có tiền", và vì sao có thể chỉ là chuyện kể
Các nghiên cứu ở Mỹ (Lakonishok & Smidt 1988, McConnell & Xu 2008) đưa ra lời giải thích rất đời. Đầu tháng có lương về và các quỹ hưu trí giải ngân, nên tiền mới chảy vào cổ phiếu. Cuối tháng thì các quỹ có động lực "làm đẹp" danh mục (window dressing, tức kéo giá các mã đang nắm trước kỳ báo cáo) cho bản báo cáo dễ nhìn hơn.
Ở Việt Nam cũng có những cơ chế na ná như vậy. Lương phần lớn về đầu tháng, còn quỹ ETF và quỹ mở báo cáo NAV vào cuối tháng. Nếu một bộ phận nhà đầu tư cá nhân cầm lương mới đi nạp tiền vào tài khoản, dòng tiền đó sẽ rơi đúng vào mấy phiên đầu tháng.
Lý do để nghi cũng nhiều không kém. Ở Mỹ, tiền lương chảy vào cổ phiếu khá tự động qua các chương trình hưu trí, còn ở mình thì ai thích nạp lúc nào thì nạp. Hơn nữa, một hiệu ứng đã nằm trong sách giáo khoa mấy chục năm thì dễ bị người ta mua đón trước, đẩy giá lên sớm, tới lúc chính hiệu ứng tự mờ đi.
Còn một điểm phải nói trước. Cửa sổ "phiên cuối tháng cộng 3 phiên đầu tháng" được lấy nguyên theo tài liệu gốc, chọn trước khi nhìn dữ liệu. Nếu mình dò hết các kiểu cửa sổ rồi chọn cái đẹp nhất thì chẳng khác gì bắn tên vào tường rồi mới vẽ hồng tâm quanh mũi tên.
Mình đo thế nào
Dữ liệu là lợi suất ngày, tính từ giá đóng cửa hôm trước đến giá đóng cửa hôm nay. Chuỗi VN30F1M có 2.143 phiên, từ 09/2017 đến 09/2026, đã bỏ các ngày đổi hợp đồng đáo hạn.
Chỉ số VN30 ngắn hơn, 1.873 phiên tính từ 04/2019. Rổ chia đều dài nhất với 2.932 phiên tính từ 02/2015, dùng giá đã điều chỉnh. Ở chuỗi nào mình cũng bỏ tháng đầu và tháng cuối, vì không đủ phiên để đếm vị trí cho đúng.
Mỗi phiên được đánh dấu là thuộc cửa sổ sang tháng hay không, rồi mình so lợi suất trung bình hai nhóm bằng Welch t-test, một phép kiểm định chuẩn để so hai trung bình. Riêng với phái sinh, mình tính thêm một quy tắc cụ thể: mua 1 hợp đồng ở giá đóng cửa phiên áp chót của tháng, bán ở giá đóng cửa phiên thứ ba của tháng mới, trừ 0,4 điểm mỗi tháng cho phí và trượt giá (40.000 đồng/hợp đồng).
Kết quả
Ngày thường so với ngày sang tháng
Đầu tiên phải xem mốc. Một phiên VN30F1M bình thường ngoài cửa sổ lãi trung bình +0,032% (n=1.712). Trong cửa sổ TOM con số là +0,182% (n=431), gấp 5,7 lần.

Hình 1: Cửa sổ sang tháng so với phần còn lại, trên ba chuỗi
Ở cả ba chuỗi, cột cam (phiên TOM) đều cao hơn cột xám (các phiên còn lại). Thanh lỗi của cột cam rộng hơn hẳn vì mỗi tháng chỉ góp được 4 phiên. Chỉ số VN30 cho kết quả rõ nhất: chênh +0,177%/phiên, t=2,31, p=0,022, mức khó đổ cho may.
Hai chuỗi còn lại thì lửng lơ hơn. Phái sinh chênh với p=0,056, rổ chia đều chênh +0,116%/phiên với p=0,052. Cả hai đứng ngay sát mốc 5% quen thuộc. Nói theo kiểu dân dã thì hướng đi đã đồng thuận, nhưng độ chắc chắn vẫn chưa tới.
Lợi suất mỗi phiên trong và ngoài cửa sổ sang tháng
| Chuỗi, nhóm phiên | Số mẫu | Trung bình | Trung vị | Tỷ lệ dương | t-stat | |:---|---:|---:|---:|---:|---:| | VN30F1M, TOM | 431 | +0,182% | +0,126% | 57,1% | 2,60 | | VN30F1M, còn lại | 1.712 | +0,032% | +0,079% | 53,7% | 0,90 | | Chỉ số VN30, TOM | 360 | +0,192% | +0,169% | 57,5% | 2,78 | | Chỉ số VN30, còn lại | 1.513 | +0,015% | +0,095% | 54,5% | 0,44 | | Rổ VN30 chia đều, TOM | 564 | +0,169% | +0,184% | 59,6% | 3,19 | | Rổ VN30 chia đều, còn lại | 2.368 | +0,054% | +0,110% | 55,7% | 2,02 |
Trong bảng này mình chỉ muốn anh em để ý cột tỷ lệ dương. Phiên TOM của VN30F1M tăng 57,1% số lần, phiên thường tăng 53,7%, chênh nhau không nhiều. Hiệu ứng chủ yếu nằm ở chỗ những phiên tăng trong cửa sổ thường tăng mạnh hơn, còn tần suất tăng thì không cao hơn là bao.
Lực kéo nằm ở phiên nào
Bước tiếp theo là tách cửa sổ ra từng phiên, xem cả bốn phiên cùng góp sức hay chỉ một phiên gánh cả nhóm.

Hình 2: VN30F1M: từng phiên quanh lúc sang tháng
Trục ngang xếp năm phiên cuối tháng ở bên trái, năm phiên đầu tháng mới ở bên phải. Đường gạch xanh là trung bình mọi phiên, +0,062%.
Các cột cam không nhô lên đều nhau. Phiên thứ hai của tháng mới cao vượt hẳn với +0,398% (t=2,82), trong khi phiên thứ ba chỉ còn +0,024%, ngang các phiên ngoài cửa sổ. Thanh lỗi của từng phiên riêng lẻ đều rộng, nên từng cột một chưa nói được gì nhiều. Gộp cả cửa sổ thì có tín hiệu, nhưng tín hiệu đó dựa khá nhiều vào một phiên.
Nhìn hình thì rất muốn đổi luật thành "chỉ đánh phiên thứ hai", nhưng mình không làm. Làm thế là quay lại trò vẽ hồng tâm.
Quy ra điểm và tiền

Hình 3: Điểm cộng dồn: trong cửa sổ TOM, ngoài cửa sổ TOM, và giữ liên tục
Đường cam là tổng điểm cộng dồn khi chỉ cầm hợp đồng trong cửa sổ TOM, tức nắm giữ 20,1% thời gian. Trước phí, đường này lên 720 điểm. Đường xanh gom bốn phần năm thời gian còn lại mà chỉ được 628 điểm. Giữ long liên tục suốt mẫu thì được 1.347 điểm.
Đổi sang cách nhìn tỷ trọng: một phần năm số phiên đóng góp 58,9% tổng lợi suất cộng dồn của VN30F1M. Sau khi trừ phí giả định, quy tắc TOM còn 676 điểm, tương đương 67.600.000 đồng/hợp đồng.
Xét theo từng tháng thì bớt lung linh hẳn. Trung bình mỗi tháng chỉ +6,20 điểm (620.200 đồng), 55,0% số tháng có lãi, t=1,87 trên 109 tháng. Cộng dồn nhiều năm trông thì đẹp, còn từng tháng một thì giống tung đồng xu có lợi thế rất nhỏ.
Tách theo giai đoạn: chỗ làm mình chột dạ nhất
Khi cắt mẫu thành ba giai đoạn thì thấy hiệu ứng không đều chút nào. Trên VN30F1M, giai đoạn 2019-2022 phiên TOM lãi +0,361% trong khi phiên thường âm -0,018%.
Sang 2023-2026 thì tình thế đảo lại: TOM chỉ +0,035%, thua cả phiên thường (+0,099%).
Rổ chia đều cũng vậy. Giai đoạn 2015-2018 TOM còn thua phiên thường, sang 2019-2022 thì TOM vọt lên +0,385%. Gần như toàn bộ hiệu ứng dồn vào giai đoạn giữa, lúc thị trường có biến động Covid và làn sóng nhà đầu tư F0 ồ ạt mở tài khoản. Chuyện "lương về thì nạp tiền" rất hợp với giai đoạn đó, nhưng hiệu ứng nào chỉ sống trong một giai đoạn thì khó mà gọi là quy luật.
Mình có thể sai ở đâu
Ba điểm mình thấy đáng lo nhất:
- Ba chuỗi không phải ba bằng chứng. Phái sinh bám theo chỉ số, còn chỉ số được tạo từ chính các mã trong rổ. Ba chuỗi cùng cho một kết quả thì chủ yếu vẫn là một bằng chứng được đo ba lần.
- Mẫu nhỏ, dễ bị vài tháng kéo lệch. Mỗi tháng chỉ có 4 phiên TOM, cả mẫu khoảng một trăm tháng. Vài tháng cực đoan như 03/2020 hay 04/2025 rơi trúng hoặc trượt khỏi cửa sổ là đủ làm trung bình xê dịch, và phần tách giai đoạn ở trên cho thấy rủi ro này là có thật.
- Đây là phép thử thứ hai trên cùng dữ liệu lịch, sau bài hiệu ứng ngày trong tuần. Thử càng nhiều hiệu ứng lịch thì càng dễ gặp một cái trông đẹp do tình cờ. Chọn cửa sổ trước giúp giảm rủi ro này, nhưng không xóa hẳn được.
Code cho ai muốn tự chạy
Toàn bộ phần tính toán và vẽ hình nằm gọn trong đoạn dưới đây.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
# daily: nến ngày VN30F1M, index = ngày, cột close và ret = lợi suất đóng->đóng
# (để NaN ở ngày đổi hợp đồng đáo hạn). Ví dụ:
# daily = pd.read_csv("vn30f1m_daily.csv", index_col="date", parse_dates=True)
# daily["ret"] = daily["close"].pct_change()
BLUE, ORANGE, NEUTRAL, INK2, AXIS = "#2a78d6", "#eb6834", "#b4b2a9", "#52514e", "#c3c2b7"
COST_PTS = 0.4
pct_axis = lambda ax, decimals=1: ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v*100:.{decimals}f}%"))
zero_line = lambda ax: ax.axhline(0, color=AXIS, linewidth=1)
def summarize(x) -> dict:
"""Số mẫu, trung bình, trung vị, tỷ lệ dương và t-stat của một chuỗi lợi suất."""
x = pd.Series(x, dtype=float).dropna()
n = len(x)
std = x.std(ddof=1) if n > 1 else np.nan
se = std / np.sqrt(n) if n > 1 else np.nan
return {
"n": n,
"mean": x.mean(),
"median": x.median(),
"std": std,
"win_rate": (x > 0).mean() if n else np.nan,
"t_stat": x.mean() / se if n > 1 and se > 0 else np.nan,
"ci95": 1.96 * se,
}
def month_position(index: pd.DatetimeIndex) -> pd.DataFrame:
"""Thứ tự của mỗi phiên trong tháng: tính từ đầu (1, 2, 3...) và từ cuối (-1 = phiên cuối tháng).
Bỏ tháng đầu và tháng cuối của mẫu vì có thể không đủ phiên (đếm sai vị trí)."""
s = pd.Series(1, index=index)
m = index.to_period("M")
pos = pd.DataFrame({"from_start": s.groupby(m).cumcount().values + 1,
"from_end": -(s.groupby(m).cumcount(ascending=False).values + 1)}, index=index)
return pos[(m != m.min()) & (m != m.max())]
def label_tom(ret: pd.Series, before: int = 1, after: int = 3) -> pd.DataFrame:
"""Gắn nhãn 'quanh lúc sang tháng' (TOM): `before` phiên cuối tháng + `after` phiên đầu tháng mới
(mặc định -1..+3, như Lakonishok & Smidt 1988). Cột `slot` = vị trí -5..-1 và +1..+5 để vẽ hồ sơ."""
d = month_position(ret.index).assign(ret=ret)
d["tom"] = (d["from_end"] >= -before) | (d["from_start"] <= after)
d["slot"] = np.where(d["from_start"] <= 5, d["from_start"], np.where(d["from_end"] >= -5, d["from_end"], 0))
return d.dropna(subset=["ret"])
def tom_points(daily: pd.DataFrame, cost_pts: float = COST_PTS) -> pd.DataFrame:
"""Quy tắc: chỉ giữ 1 hợp đồng long trong các phiên TOM (mua ở giá đóng cửa phiên -2, bán ở giá đóng cửa
phiên +3), so với giữ liên tục. Lãi/lỗ bằng điểm; bỏ ngày đổi hợp đồng (ret = NaN)."""
pts = daily["close"].diff().where(daily["ret"].notna())
d = label_tom(daily["ret"]).assign(pts=pts)
d["tom_pts"] = d["pts"].where(d["tom"], 0.0) # phiên -1, +1, +2, +3 nằm trong vị thế
d["cost"] = np.where(d["from_start"] == 3, cost_pts, 0.0) # một lượt khứ hồi mỗi tháng
return d
def plot_profile(prof: pd.DataFrame, base: float):
fig, ax = plt.subplots(figsize=(8.6, 4.6))
x = np.arange(len(prof))
colors = [ORANGE if s in (-1, 1, 2, 3) else NEUTRAL for s in prof.index]
ax.bar(x, prof["mean"], color=colors, width=0.6, zorder=2)
ax.errorbar(x, prof["mean"], yerr=prof["ci95"], fmt="none", ecolor=INK2, elinewidth=1, capsize=3, zorder=3)
ax.axhline(base, color=BLUE, ls="--", lw=1.4, zorder=4)
ax.annotate("TB mọi phiên", (len(prof) - 0.5, base), xytext=(0, 5), textcoords="offset points",
ha="right", color=INK2, fontsize=9)
ax.set_xticks(x, [f"{int(s):+d}" for s in prof.index])
ax.set_xlabel("Vị trí phiên: -5..-1 = 5 phiên cuối tháng, +1..+5 = 5 phiên đầu tháng mới")
zero_line(ax)
pct_axis(ax, decimals=2)
ax.set_title("Lợi suất trung bình theo vị trí phiên quanh lúc sang tháng (cam = cửa sổ TOM)")
return fig
d = label_tom(daily["ret"])
print(pd.DataFrame({k: summarize(g["ret"]) for k, g in d.groupby("tom")}).T[["n", "mean", "win_rate", "t_stat"]])
prof = pd.DataFrame({s: summarize(g["ret"]) for s, g in d[d["slot"] != 0].groupby("slot")}).T
pts = tom_points(daily)
print("TOM:", pts["tom_pts"].sum() - pts["cost"].sum(), "điểm; giữ liên tục:", pts["pts"].sum(), "điểm")
plot_profile(prof, d["ret"].mean()).savefig("profile.png", dpi=150)
Luồng logic khá đơn giản. Hàm month_position đếm xem mỗi phiên là phiên thứ mấy trong tháng, tính cả từ đầu tháng lẫn từ cuối tháng, đồng thời bỏ tháng đầu và tháng cuối của mẫu. Dựa vào đó, label_tom gắn nhãn TOM cho phiên cuối tháng và ba phiên đầu tháng mới, còn summarize tính số mẫu, trung bình, trung vị, tỷ lệ phiên dương và t-stat cho từng nhóm. Cuối cùng, tom_points chỉ cộng điểm của các phiên TOM, trừ phí một lần mỗi tháng vào phiên thứ ba, rồi so với tổng điểm khi giữ liên tục. Các ngày đổi hợp đồng đã để trống từ đầu nên không bị tính vào.
Dùng được gì khi giao dịch
Với mình, kết quả này là một làn gió thuận nhẹ, còn xa mới thành hệ thống. Nếu anh em vốn đã định long VN30F1M quanh lúc sang tháng thì dữ liệu không phản đối. Còn lấy riêng hiệu ứng này làm lý do vào lệnh thì lợi thế mỗi tháng chỉ vài điểm, một phiên xấu phải gồng lỗ là xóa sạch, và mấy năm gần đây nó còn không thắng nổi ngày thường.
Mức phí mình dùng chỉ là giả định và chưa tính trượt giá khi khớp ở ATC (phiên khớp lệnh định kỳ lúc đóng cửa). Với cổ phiếu cơ sở còn vướng T+2 và phí cao hơn phái sinh, nên áp cửa sổ bốn phiên lên cổ phiếu khó hơn nhiều.
Hỏi đáp
Chỉ số VN30 có t-stat trên 2 rồi, còn nghi gì nữa? t-stat cao chỉ nói rằng chênh lệch khó là do may, nó không nói chênh lệch đủ lớn để kiếm tiền hay sẽ còn tiếp diễn. Chuỗi chỉ số bắt đầu muộn, mở màn đúng vào giai đoạn hiệu ứng mạnh nhất. Phái sinh và rổ chia đều có lịch sử dài hơn thì chỉ đứng sát mốc 5%.
Trừ phí xong vẫn còn dương, sao không vào luôn cho nhanh? Tổng điểm đó là cộng dồn qua nhiều năm, và một phần lớn đến từ giai đoạn 2019-2022. Tính theo từng tháng thì chỉ nhỉnh hơn nửa số tháng có lãi. Anh em phải chịu được những chuỗi tháng thua liên tiếp mà không biết hiệu ứng đã tắt hẳn hay chỉ đang ngủ.
Rổ VN30 dùng danh sách mã hiện tại, vậy có phải tự ăn gian không? Có thiên lệch sống sót (chỉ giữ lại những mã còn trụ được trong rổ), và nó làm lợi suất chung của rổ đẹp hơn thực tế. Có điều thiên lệch này tác động lên cả phiên TOM lẫn phiên thường, nên phép so sánh giữa hai nhóm ít bị méo hơn. Thêm nữa, phái sinh và chỉ số không dính lỗi này mà vẫn đi cùng một hướng.
Nếu muốn tự thử, anh em giữ nguyên cửa sổ rồi chạy trên một bộ dữ liệu mình chưa đụng tới, ví dụ VN-Index hoặc rổ midcap. Đó mới là phép thử ngoài mẫu đúng nghĩa.
Kết
Hiệu ứng sang tháng có xuất hiện trên VN30: phiên TOM của phái sinh lãi trung bình +0,182%, phiên thường chỉ +0,032%. Nhưng bằng chứng mới ở mức sát ngưỡng, dồn gần hết vào 2019-2022, còn 2023-2026 thì gần như không thấy nữa. Câu "lương về thì thị trường có tiền" đúng được một phần, chỉ là chưa đủ để đem ra đặt cược.
Bài viết chỉ nhằm mục đích nghiên cứu và chia sẻ kiến thức, không phải khuyến nghị đầu tư.
---
Muốn tự kiểm chứng một ý tưởng như thế này? Bạn có thể dựng chiến lược, backtest và chạy paper trading ngay trên nền tảng QuantVN. Xem thêm các nghiên cứu khác trong bài tổng hợp Chứng khoán phái sinh VN30F1M: dữ liệu nói gì.