Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124

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.
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:
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?
Để đả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.
Để 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)
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ữ.
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.
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ả.
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”:
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.