Gap mở cửa VN30F1M có luôn được lấp trong ngày? Soi hơn 2.000 phiên

· · Nghiên cứu thị trường

Gap mở cửa VN30F1M có luôn được lấp trong ngày? Soi hơn 2.000 phiên

> Tóm tắt nhanh: Gap của VN30F1M được lấp trong phiên ở 71,0% số phiên. Nhưng giá đi tiếp thêm một quãng bằng gap về phía ngược lại cũng xảy ra ở 71,4% số phiên. Còn đánh ngược gap thì thắng 75,2% số phiên mà trung bình vẫn lỗ 0,82 điểm mỗi phiên, chưa tính phí.

"Gap nào rồi cũng lấp." Anh em trên diễn đàn nói câu này nhiều đến mức nghe như định luật vật lý. Sáng ra thấy phái sinh mở cửa nhảy lên vài điểm là kiểu gì cũng có người short ngay, với lý do rất gọn: đằng nào giá chả quay về.

Mình lấy nến 1 phút của VN30F1M từ 11/08/2017 đến 08/10/2026 và lọc ra 2.091 phiên có khoảng trống lúc mở cửa. Trong số đó, giá quay về lấp gap ngay trong ngày ở 71,0% số phiên. Nhìn con số này thì câu cửa miệng có vẻ đúng.

Có một câu ít ai hỏi tiếp: giá đi thêm một quãng y hệt nhưng về phía bên kia thì chuyện đó xảy ra thường xuyên cỡ nào? Câu trả lời làm câu cửa miệng lung lay khá mạnh.

> Góc người mới (biết rồi thì lướt qua) > - VN30F1M: hợp đồng tương lai trên chỉ số VN30, đáo hạn gần nhất. Không sở hữu cổ phiếu, chỉ ăn chênh lệch. Được long hay short, mua bán trong ngày, có đòn bẩy. > - Điểm và tiền: giá tính bằng điểm, mỗi điểm biến động bằng 100.000 đồng lãi/lỗ cho một hợp đồng, giống giá mỗi ký ngoài chợ. > - Gap (khoảng trống giá): chênh lệch giữa giá mở cửa hôm nay và giá đóng cửa hôm trước, tức phần giá đã "nhảy" qua đêm lúc chợ đóng. > - Lấp gap: trong phiên, giá quay ngược lại chạm đúng mức đóng cửa hôm trước. Gap càng nhỏ càng dễ bị chạm lại, chỉ vì giá lắc qua lắc lại. > - Mốc so sánh: kết quả của trường hợp "bình thường" để đặt cạnh mà so. Con được 8 điểm chỉ đáng khen nếu trung bình cả lớp là 6. > - Tỷ lệ thắng: phần trăm số lần có lãi. Thắng 9 ván mỗi ván 10 nghìn, thua 1 ván mất 200 nghìn thì vẫn lỗ.

Vì sao gap "phải" lấp, và vì sao đó có thể chỉ là ảo giác

Người tin vào câu này có lập luận nghe khá xuôi tai. Gap là phần giá phản ứng với tin qua đêm: chứng khoán Mỹ đỏ lửa, số liệu vĩ mô, tin đồn trong nhóm chat. Lúc mở cửa ai cũng vội, lệnh dồn vào vài phút đầu nên giá dễ bị đẩy quá tay, rồi khi mọi người bình tĩnh lại thì giá trôi về chỗ cũ. Ở phái sinh còn có thêm những người ôm vị thế qua đêm chốt lời ngay đầu phiên, tạo lực kéo ngược.

Phe nghi ngờ thì chỉ cần chỉ ra một điều. Gap của VN30F1M thường rất nhỏ, trung vị chỉ 0,22%, khoảng 2,6 điểm, tức 260.000 đồng mỗi hợp đồng. Với khoảng cách cỡ đó, giá chỉ cần nhích lui một chút trong ngày là đã "lấp", chẳng cần ai phản ứng thái quá. Cũng như giờ kẹt xe, xe nào rồi cũng có lúc lùi lại nửa mét, nhưng không vì thế mà bảo xe muốn quay đầu.

Muốn biết gap có "lực hút" thật hay không thì phải có mốc để so. Mốc mình chọn là mức đối xứng, nằm cách giá mở cửa đúng một khoảng bằng gap nhưng ở phía bên kia, tức là giá đi tiếp cùng chiều gap. Ví dụ sáng nay mở cửa cao hơn hôm qua x điểm: mức lấp gap nằm dưới giá mở cửa x điểm, còn mức đối xứng nằm trên x điểm. Nếu giá thật sự bị hút về chỗ cũ thì mức lấp phải được chạm thường xuyên hơn hẳn. Nếu hai tỷ lệ ngang nhau thì "lấp gap" chỉ là giá đi dạo hai chiều như mọi ngày.

Cách mình đo

Dữ liệu đã bỏ các ngày đổi hợp đồng và ngày đổi nguồn dữ liệu (gap ở những ngày này là giả do nối chuỗi giá), cùng 71 phiên mở cửa đúng bằng giá đóng cửa hôm trước. Giá mở cửa là giá khớp đầu tiên của phiên. Một gap tính là đã lấp nếu trước khi hết phiên có ít nhất một nến phút chạm lại giá đóng cửa hôm trước. Mức đối xứng được kiểm tra theo cách y hệt, chỉ ngược chiều.

Ngoài tỷ lệ lấp, mình còn thử quy tắc đơn giản nhất mà người tin "gap nào cũng lấp" sẽ dùng. Vào lệnh ngược chiều gap ngay ở giá mở cửa (gap tăng thì short, gap giảm thì long). Giá chạm mức đóng cửa hôm trước thì chốt lời, không chạm thì thoát ở giá đóng cửa phiên đó, và không đặt cắt lỗ. Chi phí giả định là 0,4 điểm mỗi lượt vào ra, tức 40.000 đồng mỗi hợp đồng.

Kết quả

Lấp gap so với mốc: gần như hòa

Tính trên tất cả các phiên, gap được lấp 71,0%, còn mức đối xứng bị chạm 71,4%. Như vậy giá quay về chỗ cũ không hề thường xuyên hơn chuyện giá đi tiếp thêm một quãng tương đương.

Chia theo độ lớn gap thì thấy rõ hơn. Trong hình dưới, mỗi cặp cột ứng với một nhóm: cột xanh là tỷ lệ lấp, cột xám là tỷ lệ chạm mức đối xứng. Muốn tin vào lực hút thì cột xanh phải cao vượt hẳn cột xám.

![Lấp gap và mức đối chứng, theo độ lớn gap](https://quantvn.com/blog-assets/vn30f1m-gap-mo-cua-co-lap-khong/fill_rates.png)

Hình 1: Lấp gap và mức đối chứng, theo độ lớn gap

Ở nhóm nào hai cột cũng đi sát nhau. Nhóm gap dưới 0,2% đông nhất với 982 phiên, lấp 87,1%, chỉ nhỉnh hơn mức đối xứng 83,5% một chút.

Từ nhóm 0,2-0,5% trở lên, mức đối xứng còn được chạm nhiều hơn cả mức lấp. Đến nhóm gap trên 1% thì giá chỉ quay về lấp ở 29,9% số phiên.

Con số chung 71% trông đẹp là do cấu trúc mẫu: nhóm gap nhỏ đông nhất, mà gap nhỏ thì gần như chắc chắn bị chạm lại. Gap càng lớn thì tỷ lệ lấp càng tụt.

Tỷ lệ lấp gap so với mức đối chứng, theo độ lớn gap

| |gap| | Số phiên | Tỷ lệ lấp gap | Tỷ lệ chạm mức đối xứng | Thời gian lấp (trung vị, phút) | |:---|---:|---:|---:|---:| | < 0,2% | 982 | 87,1% | 83,5% | 9 | | 0,2-0,5% | 682 | 65,7% | 70,1% | 52 | | 0,5-1% | 300 | 48,0% | 51,3% | 108 | | > 1% | 127 | 29,9% | 33,1% | 150 |

Lấp nhanh hay chậm

Trong hình tiếp theo, mỗi đường là một nhóm độ lớn gap và đi lên dần theo thời gian trong phiên. Đường nào dốc đứng ngay đoạn đầu thì gap nhóm đó bị lấp gần như ngay sau giờ mở cửa.

![Gap được lấp nhanh hay chậm?](https://quantvn.com/blog-assets/vn30f1m-gap-mo-cua-co-lap-khong/fill_time.png)

Hình 2: Gap được lấp nhanh hay chậm?

Gap dưới 0,2% có thời gian lấp trung vị chỉ 9 phút giao dịch. Nhóm trên 1% có trung vị 150 phút, và đường của nhóm này nằm thấp đến tận cuối phiên.

Tính trong các gap đã được lấp thì 39,2% được lấp ngay trong 15 phút đầu. Mười lăm phút đầu phiên là lúc giá lắc vài điểm qua lại như cơm bữa, nên để giải thích chuyện này thì chẳng cần đến lý thuyết phản ứng thái quá nào.

Đánh ngược gap: thắng nhiều mà vẫn lỗ

Giờ tới phần đau ví. Chưa tính chi phí, quy tắc đánh lấp gap có lãi ở 75,2% số phiên, trung vị mỗi phiên lãi +1,20 điểm. Chỉ nhìn hai con số này thì chắc nhiều anh em đã định nạp thêm tiền.

Có điều lãi/lỗ trung bình mỗi phiên lại là -0,82 điểm, với t-stat -3,88. Trị tuyệt đối gần 4 thì không đổ cho xui được, khoản lỗ này là đặc tính của quy tắc. Cộng dồn cả giai đoạn, quy tắc mất 1.719 điểm, tức 171.940.000 đồng mỗi hợp đồng, đó là chưa tính phí.

Vấn đề nằm ở độ lớn của lãi và lỗ. Những phiên gap được lấp, lệnh lãi trung bình +3,03 điểm (303.300 đồng). Những phiên gap không lấp thì giá chạy tiếp, mình ôm lệnh đến đóng cửa và lỗ trung bình -10,27 điểm (1.027.100 đồng). Y như nhặt tiền lẻ trước đầu xe lu: nhặt được nhiều lần, mỗi lần một ít, rồi có hôm bị cán.

Phiên tệ nhất mất 71,4 điểm, tức 7.140.000 đồng mỗi hợp đồng chỉ trong một ngày. Hình dưới là đường vốn, tức lãi/lỗ từng phiên cộng dồn theo thời gian.

![Lãi/lỗ cộng dồn của quy tắc 'đánh lấp gap'](https://quantvn.com/blog-assets/vn30f1m-gap-mo-cua-co-lap-khong/fade_equity.png)

Hình 3: Lãi/lỗ cộng dồn của quy tắc 'đánh lấp gap'

Đường vốn đi xuống, và những đoạn rơi thẳng đứng chính là các phiên gap không lấp mà chạy tiếp. Cộng thêm chi phí thì tổng lỗ lên tới 2.556 điểm (255.580.000 đồng mỗi hợp đồng), t-stat sau phí là -5,77.

Lọc theo nhóm cũng không cứu được. Ngay cả ở nhóm gap dưới 0,2%, nơi giá quay về gần như ngày nào cũng có, quy tắc vẫn âm 0,48 điểm mỗi phiên trước phí. Nhóm 0,5-1% tệ nhất, âm 1,62 điểm mỗi phiên.

Có phải do một giai đoạn xấu?

Dữ liệu được chia thành ba giai đoạn: trước 2021, 2021-2023 và từ 2024 trở đi. Ở cả ba, tỷ lệ lấp và tỷ lệ chạm mức đối xứng đều đi sát nhau, và quy tắc đều lỗ. Giai đoạn "nhẹ" nhất là từ 2024 trở đi, vẫn âm 0,71 điểm mỗi phiên trước phí. Kết quả không dựa vào một thời kỳ đặc biệt nào.

Những chỗ mình có thể sai

Chỗ yếu đầu tiên là giả định khớp lệnh. Mình cho rằng vào được đúng giá mở cửa và chốt được đúng giá đóng cửa hôm trước. Thực tế đầu phiên hay bị trượt giá (khớp lệch so với giá mình muốn), nhất là những phiên chạy mạnh, nên kết quả thật của quy tắc nhiều khả năng còn kém hơn trên giấy.

Các ngưỡng 0,2%, 0,5% và 1% là do mình tự chọn. Nhóm gap trên 1% chỉ có 127 phiên, đủ thấy xu hướng nhưng chưa đủ để tin chắc từng con số trong nhóm. Đổi ngưỡng thì các tỷ lệ sẽ xê dịch, nhưng ở cả bốn nhóm hiện tại cột xanh đều không bỏ xa cột xám.

Cuối cùng là nguồn dữ liệu. Dữ liệu 2017-2023 đến từ một nguồn khác (legacy), và dù mình đã loại ngày đổi nguồn với ngày đổi hợp đồng, nếu nguồn cũ có lỗi nhỏ ở giá mở cửa thì vẫn có thể sót. Giai đoạn gần nhất dùng nguồn mới mà vẫn cho kết quả cùng chiều nên mình cũng yên tâm hơn phần nào.

Code

Toàn bộ phần tính toán nằm gọn trong đoạn Python dưới đây.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter

# bars:  nến 1 phút VN30F1M với các cột date (ngày), minute, open, high, low, close
# daily: nến ngày gộp từ bars (index = ngày, cột open, high, low, close) và
#        ret = lợi suất đóng->đóng, để NaN ở ngày đổi hợp đồng đáo hạn
# daily = bars.groupby("date").agg(open=("open", "first"), high=("high", "max"),
#                                  low=("low", "min"), close=("close", "last"))

BLUE, NEUTRAL, INK2 = "#2a78d6", "#b4b2a9", "#52514e"
COST_PTS = 0.4
GAP_BINS = [0, 0.002, 0.005, 0.01, np.inf]
GAP_LABELS = ["< 0,2%", "0,2-0,5%", "0,5-1%", "> 1%"]
def pct_axis(ax, axis="y", decimals=1):
    fmt = FuncFormatter(lambda v, _: f"{v*100:.{decimals}f}%")
    (ax.yaxis if axis == "y" else ax.xaxis).set_major_formatter(fmt)


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 gap_days(daily: pd.DataFrame) -> pd.DataFrame:
    """Khoảng trống mở cửa = giá mở cửa hôm nay / giá đóng cửa hôm trước - 1.
    Bỏ ngày đổi hợp đồng và ngày đổi nguồn dữ liệu (ret = NaN), bỏ ngày mở cửa đúng bằng giá hôm trước."""
    d = daily.copy()
    d["prev_close"] = d["close"].shift(1)
    d["gap"] = (d["open"] / d["prev_close"] - 1).where(d["ret"].notna())
    d = d[d["gap"].notna() & (d["gap"] != 0)].copy()
    d["gap_bin"] = pd.cut(d["gap"].abs(), GAP_BINS, labels=GAP_LABELS)
    return d


def fill_events(bars: pd.DataFrame, gaps: pd.DataFrame) -> pd.DataFrame:
    """Với mỗi ngày có gap: giá có quay về đóng cửa hôm trước không (lấp gap), sau bao nhiêu phút giao dịch,
    và để đối chứng: giá có chạm mức 'đối xứng' cách giá mở cửa đúng một khoảng gap về phía ngược lại không."""
    b = bars[bars["date"].isin(gaps.index)].copy()
    b = b.join(gaps[["prev_close", "gap", "open"]].rename(columns={"open": "day_open"}), on="date")
    up = b["gap"] > 0
    mirror = 2 * b["day_open"] - b["prev_close"]  # cách giá mở cửa một khoảng bằng gap, đi tiếp cùng chiều gap
    b["fill"] = np.where(up, b["low"] <= b["prev_close"], b["high"] >= b["prev_close"])
    b["mirror"] = np.where(up, b["high"] >= mirror, b["low"] <= mirror)
    b["k"] = b.groupby("date").cumcount()  # số phút giao dịch kể từ mở cửa (không tính nghỉ trưa)
    g = b.groupby("date")
    ev = gaps.copy()
    ev["filled"] = g["fill"].any()
    ev["mirror_hit"] = g["mirror"].any()
    ev["fill_minute"] = b[b["fill"]].groupby("date")["k"].first()
    return ev


def fade_gap(ev: pd.DataFrame, cost_pts: float = COST_PTS) -> pd.DataFrame:
    """Quy tắc 'đánh lấp gap': vào lệnh ngược chiều gap ở giá mở cửa, chốt lời ở giá đóng cửa hôm trước
    nếu chạm, nếu không thì thoát ở giá đóng cửa. Không cắt lỗ. Lãi/lỗ tính bằng điểm/hợp đồng."""
    side = -np.sign(ev["gap"])
    exit_px = np.where(ev["filled"], ev["prev_close"], ev["close"])
    gross = side * (exit_px - ev["open"])
    return pd.DataFrame({"gross": gross, "net": gross - cost_pts, "filled": ev["filled"]}, index=ev.index)


def plot_fill_rates(tab: pd.DataFrame):
    fig, ax = plt.subplots(figsize=(8.4, 4.6))
    x = np.arange(len(tab))
    w = 0.38
    ax.bar(x - w / 2, tab["fill"], width=w - 0.03, color=BLUE, label="Quay về lấp gap", zorder=2)
    ax.bar(x + w / 2, tab["mirror"], width=w - 0.03, color=NEUTRAL, label="Chạm mức đối xứng (đối chứng)", zorder=2)
    for xi, v in zip(x - w / 2, tab["fill"]):
        ax.annotate(f"{v * 100:.0f}%", (xi, v), xytext=(0, 3), textcoords="offset points", ha="center",
                    color=INK2, fontsize=9)
    ax.set_xticks(x, [f"{k}\n{int(n)} phiên" for k, n in zip(tab.index, tab["n"])])
    ax.set_xlabel("Độ lớn khoảng trống mở cửa (|gap|)")
    ax.set_ylim(0, 1.05)
    pct_axis(ax, decimals=0)
    ax.set_title("Tỷ lệ phiên giá chạm mức tham chiếu trước giờ đóng cửa")
    ax.legend(loc="upper right")
    return fig


ev = fill_events(bars, gap_days(daily))
tab = ev.groupby("gap_bin", observed=True).agg(n=("filled", "size"), fill=("filled", "mean"),
                                               mirror=("mirror_hit", "mean"))
print(tab)
pnl = fade_gap(ev)
print(summarize(pnl["gross"]), pnl[["gross", "net"]].sum())
plot_fill_rates(tab).savefig("fill_rates.png", dpi=150)

Hàm gap_days lấy giá mở cửa chia cho giá đóng cửa hôm trước rồi trừ 1 để ra gap, bỏ các ngày đổi hợp đồng, ngày đổi nguồn và ngày gap bằng 0, rồi xếp mỗi phiên vào một trong bốn nhóm độ lớn. Tiếp theo, fill_events soát từng nến phút: gap tăng thì xem giá thấp nhất có chạm đóng cửa hôm trước không, gap giảm thì xem giá cao nhất. Song song đó nó kiểm tra mức đối xứng (hai lần giá mở cửa trừ giá đóng cửa hôm trước) và ghi lại phút đầu tiên gap được lấp. Hàm fade_gap vào lệnh ngược dấu gap, thoát ở giá đóng cửa hôm trước nếu đã lấp, không thì thoát ở giá đóng cửa, và tính thêm bản đã trừ 0,4 điểm phí. Cuối cùng summarize tính số mẫu, trung bình, trung vị, tỷ lệ thắng và t-stat.

Vậy dùng được gì khi giao dịch

Bài học thực tế nhất là đừng lấy câu "gap rồi sẽ lấp" làm cớ để gồng lỗ. Với gap lớn hơn 0,5%, giá chỉ quay về ở 42,6% trong 427 phiên, và những phiên không quay về chính là những cú rơi thẳng đứng trên đường vốn. Đang cầm lệnh ngược chiều một gap lớn thì nên coi đó là gió ngược.

Với gap nhỏ, giá chạm lại mức đóng cửa hôm trước gần như là chuyện đương nhiên. Biết vậy để khỏi hoảng khi thấy giá "quay đầu" mấy phút đầu phiên, chứ đừng coi đó là tín hiệu. Phái sinh có đòn bẩy, mỗi lượt vào ra lại tốn khoảng 40.000 đồng, nên một lợi thế mỏng bị ăn hết rất nhanh.

Hỏi đáp

Gap nhỏ lấp tới 87% mà, chỉ đánh gap nhỏ thì sao? Mức đối xứng của nhóm này cũng bị chạm gần như thường xuyên ngang vậy, tức giá lắc hai chiều đều nhau. Như đã thấy ở trên, quy tắc trên nhóm này vẫn âm mỗi phiên ngay cả trước phí, trừ thêm phí thì còn tệ hơn.

Không đánh ngược được thì đánh thuận chiều gap? Cũng chẳng có gì để ăn. Lợi suất từ mở cửa đến đóng cửa là +0,013% sau gap tăng và -0,014% sau gap giảm, p-value 0,662, khác biệt này hoàn toàn có thể do ngẫu nhiên. Vào ngược chiều gap rồi giữ đến đóng cửa cũng chỉ ra -0,10 điểm mỗi phiên, vẫn trong vùng nhiễu.

Thêm cắt lỗ là ra lời chứ gì? Đường vốn có thể bớt mấy cú rơi thẳng đứng, nhưng mỗi mức cắt lỗ là thêm một tham số, mà thử đủ nhiều mức thì kiểu gì cũng tìm ra một con số đẹp trên dữ liệu quá khứ. Cắt lỗ cũng không giải quyết được gốc rễ là tỷ lệ lấp không cao hơn mốc. Anh em muốn tự thử thì chọn trước một mức cắt lỗ cố định rồi kiểm tra riêng từng giai đoạn, đừng dò mãi đến khi ra số đẹp.

Kết

Gap của VN30F1M đúng là được lấp ở 71,0% số phiên, nhưng giá đi tiếp thêm một quãng bằng gap cũng xảy ra ở 71,4% số phiên. Câu cửa miệng đúng về con số nhưng hiểu sai ý nghĩa. Đánh ngược gap thắng được nhiều lần, có điều mỗi lần thua lớn lại xóa sạch lãi của nhiều lần thắng, nên cộng dồn cả giai đoạn vẫn lỗ.

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ì.