Phân Tích Định Lượng Về Hành Vi Giá “Gap and Go” Trên Cổ Phiếu S&P 500


Chào mừng các bạn đã quay trở lại blog. Sau một thời gian tạm vắng, tôi rất vui khi được kết nối lại cùng cộng đồng. Lần này, tôi muốn chia sẻ một nghiên cứu định lượng chuyên sâu nhưng không kém phần thú vị về hành vi giá ‘Gap and Go’ của các cổ phiếu S&P 500. Toàn bộ bài phân tích sẽ được trình bày bằng tiếng Việt, với mong muốn mang những kiến thức dựa trên dữ liệu đến gần hơn với các nhà giao dịch tại Việt Nam.

Giới thiệu

Trong giao dịch theo xu hướng, mô hình “Gap and Go” là một trong những tín hiệu được công nhận rộng rãi. Ý tưởng cơ bản là khi một cổ phiếu mở cửa với một khoảng trống giá (gap) đáng kể so với phiên trước, nó có xu hướng tiếp tục di chuyển theo hướng của khoảng trống đó.

Bài viết này thực hiện một phân tích định lượng để kiểm tra tính hợp lệ của giả định trên. Chúng ta sẽ xem xét hai kịch bản:

  • Gap Up & Go: Một tín hiệu được cho là tăng giá (bullish).
  • Gap Down & Go: Một tín hiệu được cho là giảm giá (bearish).

Câu hỏi trọng tâm của chúng tôi là: Liệu các hành vi giá này có thực sự mang lại một lợi thế có thể đo lường và có ý nghĩa thống kê hay không?

Phương pháp luận

Để đảm bảo tính khách quan, nghiên cứu được tiến hành với một bộ quy tắc xác định rõ ràng.

  • Đối tượng nghiên cứu: Các cổ phiếu thành phần của chỉ số S&P 500.
  • Khung thời gian: Dữ liệu giao dịch hàng ngày trong 5 năm, từ tháng 8/2020 đến tháng 8/2025.
  • Định nghĩa tín hiệu: Một ngày giao dịch được xác định là một tín hiệu nếu nó đáp ứng tất cả các tiêu chí sau:
    1. Điều kiện Gap:
      • Gap Up: Giá mở cửa hôm nay > Giá cao nhất hôm qua.
      • Gap Down: Giá mở cửa hôm nay < Giá thấp nhất hôm qua.
    2. Bộ lọc Kích thước Gap: Khoảng trống giá phải lớn hơn 1% so với giá đóng cửa của ngày hôm trước để được xem là có ý nghĩa.
    3. Bộ lọc Khối lượng: Khối lượng giao dịch của ngày tín hiệu phải cao hơn 1.5 lần so với khối lượng trung bình 20 ngày trước đó, nhằm xác nhận sự quan tâm của thị trường.
    4. Xác nhận “Go”:
      • Gap Up: Giá đóng cửa > Giá mở cửa.
      • Gap Down: Giá đóng cửa < Giá mở cửa.
  • Khuôn khổ Phân tích: Chúng tôi đo lường lợi nhuận kỳ vọng trong các chu kỳ nắm giữ khác nhau (1, 2, 3, 5, 10, 20, và 40 ngày). Lợi nhuận của nhóm “ngày tín hiệu” sẽ được so sánh với nhóm “ngày cơ sở” (tất cả các ngày giao dịch còn lại). Các chỉ số chính bao gồm Lợi nhuận Vượt trội (Excess Return)giá trị p (p-value) để đánh giá ý nghĩa thống kê.

Để cụ thể hóa các điều kiện đã nêu, đây là đoạn mã Python dùng để xác định tín hiệu ‘Gap Up & Go’. Logic cho ‘Gap Down and Go’ cũng được xây dựng theo cấu trúc tương tự.:

import pandas as pd
import numpy as np
import yfinance as yf
from scipy import stats
from statsmodels.stats.proportion import proportions_ztest

# 1. DATA ACQUISITION
# Programmatically get the list of S&P 500 tickers from Wikipedia
payload = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')
sp500_tickers = payload[0]['Symbol'].tolist()
sp500_tickers = [ticker.replace('.', '-') for ticker in sp500_tickers]

print(f"Found {len(sp500_tickers)} tickers. Downloading data...")

# Download the data for the fetched tickers up to the current date.
data = yf.download(sp500_tickers, start="2020-08-17", end="2025-08-18", group_by='ticker')

print("Data download complete.")

all_results = []
# Loop through each downloaded stock
for ticker in sp500_tickers:
    df = data.get(ticker)
    if df is None or df.empty:
        continue

    # 2. FEATURE ENGINEERING
    df['Close_prev'] = df['Close'].shift(1)
    df['High_prev'] = df['High'].shift(1)
    df['Volume_MA20'] = df['Volume'].shift(1).rolling(window=20).mean()

    # 3. SIGNAL DEFINITION (Bullish "Gap Up & Go")
    gap_up_cond = (df['Open'] > df['High_prev'])
    gap_size_cond = ((df['Open'] - df['High_prev']) / df['Close_prev']) > 0.01
    volume_cond = (df['Volume'] > 1.5 * df['Volume_MA20'])
    go_cond = (df['Close'] > df['Open'])

    df['signal'] = gap_up_cond & gap_size_cond & volume_cond & go_cond

    # 4. CALCULATE FORWARD RETURNS FOR ALL DAYS
    offsets = [1, 2, 3, 5, 10, 20, 40]
    for n in offsets:
        df[f'fwd_ret_{n}d'] = df['Close'].shift(-n) / df['Close'] - 1
    
    all_results.append(df)

# 5. AGGREGATE AND ANALYZE
combined_df = pd.concat(all_results)

analysis_results = []
for n in offsets:
    col_name = f'fwd_ret_{n}d'
    
    signal_returns = combined_df[combined_df['signal'] == True][col_name].dropna()
    baseline_returns = combined_df[combined_df['signal'] == False][col_name].dropna()
    
    if len(signal_returns) < 2 or len(baseline_returns) < 2:
        continue

    # Perform calculations for returns
    avg_sig = signal_returns.mean()
    avg_base = baseline_returns.mean()
    std_sig = signal_returns.std()
    
    # Perform calculations for win rates (proportions)
    successes_sig = (signal_returns > 0).sum()
    nobs_sig = len(signal_returns)
    days_up_sig = successes_sig / nobs_sig

    successes_base = (baseline_returns > 0).sum()
    nobs_base = len(baseline_returns)
    days_up_base = successes_base / nobs_base
    
    # Perform statistical tests
    ttest_result = stats.ttest_ind(signal_returns, baseline_returns, equal_var=False, nan_policy='omit')
    ks_result = stats.ks_2samp(signal_returns, baseline_returns)

    # Calculate the Z-test for proportions
    count = np.array([successes_sig, successes_base])
    nobs = np.array([nobs_sig, nobs_base])
    
    ### CORRECTED LINE ###
    # Removed the 'nan_policy' argument for compatibility with older statsmodels versions
    z_stat, p_value_z = proportions_ztest(count, nobs)
    
    analysis_results.append({
        'Offset': f'{n} Days',
        'N_sig': nobs_sig,
        'Avg_sig (bps)': avg_sig * 10000,
        'Avg_base (bps)': avg_base * 10000,
        'Excess (bps)': (avg_sig - avg_base) * 10000,
        'Std_sig (bps)': std_sig * 10000,
        'DaysUp_sig': f"{days_up_sig:.1%}",
        'DaysUp_base': f"{days_up_base:.1%}",
        'T-test p-value': ttest_result.pvalue,
        'KS p-value': ks_result.pvalue,
        'Z-test p-value': p_value_z,
    })

# 6. DISPLAY RESULTS
results_df = pd.DataFrame(analysis_results).set_index('Offset')
print("\n--- Bullish 'Gap Up & Go' Strategy Results (with Z-Test) ---")
print(results_df)

Kết Quả Phân Tích

Phần 1: Kết quả kiểm chứng “Gap Up & Go” (Kịch bản Tăng giá)

Phân tích cho thấy mô hình này có một lợi thế nhất định, nhưng hiệu quả của nó phụ thuộc rất nhiều vào chu kỳ nắm giữ.

  • Chu kỳ nắm giữ ngắn hạn (1-3 ngày): Lợi nhuận vượt trội trong khoảng thời gian này là rất nhỏ và không có ý nghĩa thống kê (giá trị p-value cao). Dữ liệu cho thấy tín hiệu không có khả năng dự báo đáng tin cậy cho các giao dịch rất ngắn hạn.
  • Chu kỳ nắm giữ trung hạn (5-20 ngày): Một lợi thế rõ rệt và có ý nghĩa thống kê bắt đầu xuất hiện. Lợi nhuận vượt trội đạt đỉnh tại mốc 20 ngày, cao hơn khoảng +89.6 điểm cơ bản (+0.90%) so với nhóm cơ sở. Giá trị p-value cực thấp trong giai đoạn này khẳng định rằng kết quả này không phải là ngẫu nhiên. Tỷ lệ các ngày tăng giá của nhóm tín hiệu cũng cao hơn một cách có ý nghĩa so với nhóm cơ sở.
  • Chu kỳ nắm giữ dài hạn (40 ngày): Lợi thế này giảm dần và trở nên không còn ý nghĩa thống kê ở mốc 40 ngày.

Diễn giải: Mô hình “Gap Up & Go”, theo định nghĩa của chúng tôi, không phải là một tín hiệu hiệu quả cho giao dịch trong ngày hoặc vài ngày. Thay vào đó, nó xác định các điểm khởi đầu của một đà tăng giá có xu hướng kéo dài trong khoảng từ 2 đến 4 tuần.

Phần 2: Kết quả kiểm chứng “Gap Down & Go” (Kịch bản Giảm giá)

Kết quả của thử nghiệm này lại trái ngược hoàn toàn với giả thuyết ban đầu.

  • Giả thuyết bị bác bỏ: Thay vì cho thấy lợi nhuận âm, các cổ phiếu sau tín hiệu “Gap Down & Go” lại ghi nhận mức sinh lời vượt trội và dương một cách nhất quán trên tất cả các chu kỳ nắm giữ.
  • Hiệu suất vượt trội: Lợi nhuận vượt trội so với nhóm cơ sở là rất lớn, đạt tới +210 điểm cơ bản ở mốc 20 ngày và +328 điểm cơ bản (+3.28%) ở mốc 40 ngày.
  • Ý nghĩa thống kê: Các kiểm định thống kê (T-test, KS-test) đều cho ra giá trị p-value gần như bằng không trên mọi khung thời gian. Điều này cung cấp bằng chứng mạnh mẽ rằng hiệu suất vượt trội này là một đặc tính thực sự của thị trường, không phải là một sai số ngẫu nhiên.

Diễn giải: Dữ liệu cho thấy mô hình “Gap Down & Go” không phải là tín hiệu cho thấy xu hướng tiếp tục giảm của cổ phiếu. Ngược lại, nó dường như đánh dấu một sự kiện bán tháo cuối cùng (capitulation), sau đó lực mua quay trở lại và đẩy giá phục hồi mạnh mẽ. Do đó, tín hiệu này hoạt động như một chỉ báo mua ngược xu hướng (contrarian bullish) hiệu quả.


Kết Luận

Phân tích định lượng này cung cấp một số hiểu biết quan trọng về hành vi giá “Gap and Go”:

  1. Hiệu quả phụ thuộc vào chu kỳ nắm giữ: Tín hiệu “Gap Up & Go” cho thấy một lợi thế tăng giá có ý nghĩa thống kê, nhưng chỉ trong khung thời gian trung hạn (10-20 ngày). Nó không đáng tin cậy cho các giao dịch ngắn hạn hơn.
  2. Khám phá một tín hiệu ngược xu hướng: Trái với kỳ vọng, tín hiệu “Gap Down & Go” không phải là một chỉ báo giảm giá. Thay vào đó, nó là một tín hiệu mua vào mạnh mẽ, thường báo trước một giai đoạn phục hồi giá kéo dài.

Nghiên cứu này nhấn mạnh tầm quan trọng của việc kiểm chứng các giả định giao dịch bằng dữ liệu thực tế. Một mô hình có vẻ hợp lý về mặt lý thuyết có thể hoạt động khác đi trong thực tế, và đôi khi, những kết quả bất ngờ nhất lại mang đến những lợi thế giao dịch giá trị nhất.

Tuyên bố miễn trừ trách nhiệm: Nội dung trong bài viết này chỉ dành cho mục đích nghiên cứu và giáo dục, không được xem là lời khuyên đầu tư. Giao dịch tài chính luôn tiềm ẩn rủi ro.

minhchaudkhbt97@gmail.com
minhchaudkhbt97@gmail.com
Articles: 4