Chương này giới thiệu một trong những khái niệm cơ bản nhất của kinh tế học tài chính: mức độ phơi nhiễm của một cổ phiếu riêng lẻ trước rủi ro thị trường mang tính hệ thống. Theo mô hình định giá tài sản vốn (CAPM) do Sharpe (1964), Lintner (1965) và Mossin (1966) phát triển, sự khác biệt theo chiều ngang trong lợi nhuận kỳ vọng của các tài sản phải được quyết định bởi hiệp phương sai giữa lợi nhuận vượt mức của tài sản và lợi nhuận vượt mức của danh mục thị trường. Hệ số hồi quy nắm bắt mối quan hệ này (thường được gọi là beta thị trường) là nền tảng của lý thuyết danh mục đầu tư hiện đại và vẫn được sử dụng rộng rãi trong thực tiễn để ước lượng chi phí vốn, phân tích đóng góp hiệu quả đầu tư và quản trị rủi ro.
Trong chương này, chúng ta xây dựng một khung phân tích hoàn chỉnh để ước lượng beta thị trường cho các cổ phiếu Việt Nam. Chúng ta bắt đầu bằng một tổng quan khái niệm về CAPM và việc triển khai mô hình này trong thực nghiệm. Sau đó, chúng ta minh họa việc ước lượng beta bằng hồi quy bình phương nhỏ nhất thông thường, trước hết cho từng cổ phiếu riêng lẻ, rồi mở rộng ra toàn thị trường bằng ước lượng cửa sổ trượt. Để đáp ứng yêu cầu tính toán khi ước lượng beta cho hàng trăm cổ phiếu qua nhiều kỳ, chúng ta giới thiệu các kỹ thuật song song hóa giúp giảm đáng kể thời gian xử lý. Cuối cùng, chúng ta so sánh các ước lượng beta thu được từ lợi nhuận tháng với lợi nhuận ngày, và xem xét beta thay đổi như thế nào giữa các ngành và theo thời gian trên thị trường Việt Nam.
Chương này vận dụng một số khái niệm tính toán quan trọng vượt ra ngoài phạm vi ước lượng beta. Ước lượng cửa sổ trượt là kỹ thuật có thể áp dụng cho mọi tham số biến thiên theo thời gian, còn song song hóa là giải pháp chung cho các tác vụ đòi hỏi nhiều tính toán nhưng có thể chia thành các tác vụ con độc lập.
15.1 Nền tảng lý thuyết
15.1.1 Mô hình định giá tài sản vốn
CAPM cung cấp một khung lý thuyết liên kết lợi nhuận kỳ vọng với rủi ro hệ thống. Theo các giả định của mô hình, bao gồm nhà đầu tư tối ưu hóa trung bình–phương sai, kỳ vọng đồng nhất và thị trường không có ma sát, lợi nhuận vượt mức kỳ vọng của bất kỳ tài sản \(i\) nào tỷ lệ thuận với hiệp phương sai của nó với danh mục thị trường:
\[
E[r_i - r_f] = \beta_i \cdot E[r_m - r_f]
\]
trong đó \(r_i\) là lợi nhuận của tài sản \(i\), \(r_f\) là lãi suất phi rủi ro, \(r_m\) là lợi nhuận của danh mục thị trường, và \(\beta_i\) được định nghĩa như sau:
Beta thị trường \(\beta_i\) đo độ nhạy của lợi nhuận tài sản \(i\) trước các biến động của thị trường. Beta lớn hơn một cho thấy tài sản khuếch đại các biến động của thị trường, trong khi beta nhỏ hơn một cho thấy độ nhạy thấp hơn. Beta bằng không hàm ý không có phơi nhiễm rủi ro hệ thống, chỉ còn lại rủi ro đặc thù có thể được triệt tiêu thông qua đa dạng hóa.
15.1.2 Triển khai trong thực nghiệm
Trong thực tế, chúng ta ước lượng beta bằng cách hồi quy lợi nhuận vượt mức của cổ phiếu theo lợi nhuận vượt mức của thị trường:
trong đó \(\alpha_i\) biểu diễn lợi nhuận bất thường (alpha của Jensen), \(\beta_i\) là beta thị trường cần ước lượng, và \(\varepsilon_{i,t}\) là số hạng sai số đặc thù. Theo CAPM, \(\alpha_i\) phải bằng không đối với mọi tài sản; bất kỳ alpha khác không nào cũng là sự lệch khỏi dự báo của mô hình.
Một số cân nhắc thực tiễn ảnh hưởng đến việc ước lượng beta:
Cửa sổ ước lượng: Cửa sổ dài hơn cho nhiều quan sát hơn và do đó ước lượng chính xác hơn, nhưng có thể chứa thông tin lỗi thời nếu beta thay đổi theo thời gian. Lựa chọn phổ biến nằm trong khoảng 36 đến 60 tháng đối với dữ liệu tháng.
Tần suất lợi nhuận: Lợi nhuận tháng giảm nhiễu nhưng cho ít quan sát hơn. Lợi nhuận ngày cho nhiều điểm dữ liệu hơn nhưng có thể gây ra các hiệu ứng vi cấu trúc và thiên lệch do giao dịch không đồng bộ.
Đại diện thị trường: Danh mục thị trường lý thuyết bao gồm mọi tài sản, nhưng trong thực tế chúng ta dùng một chỉ số cổ phiếu rộng. Đối với Việt Nam, chúng ta dùng lợi nhuận thị trường trọng số theo vốn hóa được xây dựng từ tập hợp cổ phiếu của chúng ta.
Số quan sát tối thiểu: Yêu cầu một số quan sát tối thiểu (ví dụ 48 trên 60 tháng) giúp tránh các ước lượng thiếu tin cậy do dữ liệu thưa.
15.2 Thiết lập môi trường
Chúng ta bắt đầu bằng việc nạp các gói Python cần thiết. Các gói cốt lõi đảm nhiệm xử lý dữ liệu, mô hình thống kê và các thao tác với cơ sở dữ liệu. Chúng ta cũng nhập các công cụ song song hóa, vốn sẽ rất cần thiết khi mở rộng việc ước lượng ra toàn thị trường.
Hiện mã
import pandas as pdimport numpy as npimport sqlite3import statsmodels.formula.api as smffrom scipy.stats.mstats import winsorizefrom plotnine import*from mizani.formatters import percent_format, comma_formatfrom joblib import Parallel, delayed, cpu_countfrom dateutil.relativedelta import relativedelta
Chúng ta kết nối với cơ sở dữ liệu SQLite chứa dữ liệu tài chính Việt Nam đã xử lý từ các chương trước.
Chúng ta nạp dữ liệu lợi nhuận cổ phiếu theo tháng đã được chuẩn bị trong chương Datacore. Dữ liệu bao gồm lợi nhuận vượt mức (lợi nhuận trừ đi lãi suất phi rủi ro) của tất cả cổ phiếu niêm yết tại Việt Nam.
Hiện mã
prices_monthly = pd.read_sql_query( sql=""" SELECT symbol, date, ret_excess FROM prices_monthly """, con=tidy_finance, parse_dates={"date"})# Add year for merging with fundamentalsprices_monthly["year"] = prices_monthly["date"].dt.yearprint(f"Loaded {len(prices_monthly):,} monthly observations")print(f"Covering {prices_monthly['symbol'].nunique():,} unique stocks")print(f"Date range: {prices_monthly['date'].min():%Y-%m} to {prices_monthly['date'].max():%Y-%m}")
Loaded 209,495 monthly observations
Covering 1,837 unique stocks
Date range: 2010-01 to 2025-05
Chúng ta nạp thông tin doanh nghiệp để phục vụ phân tích các ước lượng beta ở cấp độ ngành.
Hiện mã
comp_vn = pd.read_sql_query( sql=""" SELECT symbol, datadate, icb_name_vi FROM comp_vn """, con=tidy_finance, parse_dates={"datadate"})# Extract year for mergingcomp_vn["year"] = comp_vn["datadate"].dt.yearprint(f"Company data: {comp_vn['symbol'].nunique():,} firms")
Company data: 1,502 firms
15.3.3 Lợi nhuận vượt mức của thị trường
Làm đại diện cho danh mục thị trường, chúng ta dùng lợi nhuận vượt mức của thị trường theo trọng số vốn hóa. Nếu bạn đã xây dựng các nhân tố Fama-French ở một chương trước, hãy nạp chúng tại đây. Nếu không, chúng ta có thể xây dựng một lợi nhuận thị trường đơn giản từ dữ liệu cổ phiếu của mình.
Hiện mã
# Option 1: Load pre-computed market factorfactors_ff3_monthly = pd.read_sql_query( sql="SELECT date, mkt_excess FROM factors_ff3_monthly", con=tidy_finance, parse_dates={"date"})# Option 2: Construct market return from stock data (if factors not available)# This computes the value-weighted average return across all stocksdef compute_market_return(prices_df):""" Compute value-weighted market return from individual stock returns. Parameters ---------- prices_df : pd.DataFrame Stock returns with mktcap_lag for weighting Returns ------- pd.DataFrame Monthly market excess returns """ market_return = (prices_df .groupby("date") .apply(lambda x: np.average(x["ret_excess"], weights=x["mktcap_lag"])) .reset_index(name="mkt_excess") )return market_return
15.3.4 Hợp nhất các bộ dữ liệu
Chúng ta kết hợp lợi nhuận cổ phiếu với lợi nhuận thị trường và thông tin doanh nghiệp để tạo ra bộ dữ liệu phục vụ ước lượng.
Hiện mã
# Merge stock returns with market returnsprices_monthly = prices_monthly.merge( factors_ff3_monthly, on="date", how="left")# Merge with company information for industry classificationprices_monthly = prices_monthly.merge( comp_vn[["symbol", "year", "icb_name_vi"]], on=["symbol", "year"], how="left")# Remove observations with missing dataprices_monthly = prices_monthly.dropna(subset=["ret_excess", "mkt_excess"])print(f"Final estimation sample: {len(prices_monthly):,} observations")
Final estimation sample: 169,983 observations
15.3.5 Xử lý các giá trị ngoại lai
Các mức lợi nhuận cực đoan có thể ảnh hưởng quá mức đến kết quả ước lượng hồi quy. Chúng ta áp dụng winsorization (cắt đuôi giá trị cực đoan) để hạn chế tác động của các giá trị ngoại lai trong khi vẫn giữ nguyên phân phối chung của lợi nhuận. Winsorization ở mức 1% thay các giá trị thấp hơn phân vị thứ 1 bằng giá trị phân vị thứ 1, và các giá trị cao hơn phân vị thứ 99 bằng giá trị phân vị thứ 99.
Hiện mã
def winsorize_returns(df, columns, limits=(0.01, 0.01)):""" Apply winsorization to return columns to limit outlier influence. Parameters ---------- df : pd.DataFrame DataFrame containing return columns columns : list Column names to winsorize limits : tuple Lower and upper percentile limits for winsorization Returns ------- pd.DataFrame DataFrame with winsorized columns """ df = df.copy()for col in columns: df[col] = winsorize(df[col], limits=limits)return dfprices_monthly = winsorize_returns( prices_monthly, columns=["ret_excess", "mkt_excess"], limits=(0.01, 0.01))print("Return distributions after winsorization:")print(prices_monthly[["ret_excess", "mkt_excess"]].describe().round(4))
Return distributions after winsorization:
ret_excess mkt_excess
count 169983.0000 169983.0000
mean 0.0011 -0.0102
std 0.1548 0.0579
min -0.4078 -0.1794
25% -0.0700 -0.0384
50% -0.0033 -0.0084
75% 0.0531 0.0219
max 0.6117 0.1221
15.4 Ước lượng beta cho từng cổ phiếu
15.4.1 Ví dụ với một cổ phiếu
Trước khi mở rộng ra toàn thị trường, chúng ta minh họa việc ước lượng beta cho một cổ phiếu nổi tiếng của Việt Nam. Chúng ta dùng Vingroup (VIC), một trong những tập đoàn lớn nhất Việt Nam với mức độ hiện diện đáng kể trong các lĩnh vực bất động sản, bán lẻ và ô tô.
Hiện mã
# Filter data for Vingroupvic_data = prices_monthly.query("symbol == 'VIC'").copy()print(f"VIC observations: {len(vic_data)}")print(f"Date range: {vic_data['date'].min():%Y-%m} to {vic_data['date'].max():%Y-%m}")
VIC observations: 150
Date range: 2011-07 to 2023-12
Chúng ta ước lượng hồi quy CAPM bằng phương pháp bình phương nhỏ nhất thông thường thông qua gói statsmodels. Giao diện công thức (formula) cung cấp một cách thuận tiện để chỉ định các mô hình hồi quy.
Chúng ta tạo một hàm có thể tái sử dụng, ước lượng CAPM và trả về kết quả theo định dạng chuẩn hóa. Hàm này bao gồm yêu cầu về số quan sát tối thiểu để tránh các ước lượng thiếu tin cậy do dữ liệu thưa.
Hiện mã
def estimate_capm(data, min_obs=48):""" Estimate CAPM regression and return coefficients. This function regresses excess stock returns on excess market returns and extracts the coefficient estimates along with t-statistics. Parameters ---------- data : pd.DataFrame DataFrame with 'ret_excess' and 'mkt_excess' columns min_obs : int Minimum number of observations required for estimation Returns ------- pd.DataFrame DataFrame with coefficient estimates and t-statistics, or empty DataFrame if insufficient observations """iflen(data) < min_obs:return pd.DataFrame()try:# Estimate OLS regression model = smf.ols( formula="ret_excess ~ mkt_excess", data=data ).fit()# Extract coefficient table coef_table = model.summary2().tables[1]# Format results results = pd.DataFrame({"coefficient": ["alpha", "beta"],"estimate": [ coef_table.loc["Intercept", "Coef."], coef_table.loc["mkt_excess", "Coef."] ],"t_statistic": [ coef_table.loc["Intercept", "t"], coef_table.loc["mkt_excess", "t"] ],"r_squared": model.rsquared })return resultsexceptExceptionas e:# Return empty DataFrame if estimation failsreturn pd.DataFrame()
15.5 Ước lượng cửa sổ trượt
15.5.1 Động cơ của cửa sổ trượt
Beta của cổ phiếu không cố định theo thời gian. Cơ cấu kinh doanh, đòn bẩy và môi trường hoạt động của doanh nghiệp thay đổi, khiến mức độ phơi nhiễm rủi ro hệ thống của nó cũng thay đổi. Để nắm bắt sự biến thiên theo thời gian này, chúng ta dùng ước lượng cửa sổ trượt: tại mỗi thời điểm, chúng ta ước lượng beta chỉ bằng dữ liệu từ một giai đoạn nhìn lại cố định (ví dụ 60 tháng gần nhất).
Ước lượng cửa sổ trượt đòi hỏi một sự đánh đổi:
Cửa sổ dài hơn cho nhiều quan sát hơn và do đó ước lượng chính xác hơn, nhưng có thể chứa thông tin cũ.
Cửa sổ ngắn hơn phản ứng nhanh hơn với các thay đổi nhưng cho ước lượng nhiễu hơn.
Một lựa chọn phổ biến trong nghiên cứu học thuật là 60 tháng (5 năm) dữ liệu tháng, đòi hỏi ít nhất 48 quan sát hợp lệ để ước lượng.
15.5.2 Triển khai cửa sổ trượt
Hàm sau triển khai ước lượng CAPM theo cửa sổ trượt. Với mỗi tháng trong mẫu, hàm nhìn lại trong cửa sổ đã chỉ định và ước lượng beta bằng toàn bộ dữ liệu sẵn có trong cửa sổ đó.
Hiện mã
def roll_capm_estimation(data, look_back=60, min_obs=48):""" Perform rolling-window CAPM estimation. This function slides a window across time, estimating the CAPM regression at each point using the most recent 'look_back' months of data. Parameters ---------- data : pd.DataFrame DataFrame with 'date', 'ret_excess', and 'mkt_excess' columns look_back : int Number of months in the estimation window min_obs : int Minimum observations required within each window Returns ------- pd.DataFrame Time series of coefficient estimates with dates """# Ensure data is sorted by date data = data.sort_values("date").copy()# Get unique dates dates = data["date"].drop_duplicates().sort_values()# Container for results results = []# Slide window across datesfor i inrange(look_back -1, len(dates)):# Define window boundaries end_date = dates.iloc[i] start_date = end_date - relativedelta(months=look_back -1)# Extract data within window window_data = data.query("date >= @start_date and date <= @end_date")# Estimate CAPM for this window window_results = estimate_capm(window_data, min_obs=min_obs)ifnot window_results.empty: window_results["date"] = end_date results.append(window_results)# Combine all resultsif results:return pd.concat(results, ignore_index=True)else:return pd.DataFrame()
15.5.3 Ví dụ: beta trượt của một số cổ phiếu được chọn
Chúng ta minh họa ước lượng cửa sổ trượt cho một số cổ phiếu nổi tiếng của Việt Nam thuộc các ngành khác nhau.
Hiện mã
# Define example stocksexamples = pd.DataFrame({"symbol": ["FPT", "VNM", "VIC", "HPG", "VCB"],"company": ["FPT Corporation", # Technology"Vinamilk", # Consumer goods"Vingroup", # Real estate/conglomerate"Hoa Phat Group", # Steel/materials"Vietcombank"# Banking ]})# Check data availability for each exampledata_availability = (prices_monthly .query("symbol in @examples['symbol']") .groupby("symbol") .agg( n_obs=("date", "count"), first_date=("date", "min"), last_date=("date", "max") ) .reset_index())print("Data availability for example stocks:")print(data_availability)
Data availability for example stocks:
symbol n_obs first_date last_date
0 FPT 150 2011-07-31 2023-12-31
1 HPG 150 2011-07-31 2023-12-31
2 VCB 150 2011-07-31 2023-12-31
3 VIC 150 2011-07-31 2023-12-31
4 VNM 150 2011-07-31 2023-12-31
Hiện mã
# Estimate rolling betas for example stocksexample_data = prices_monthly.query("symbol in @examples['symbol']")capm_examples = (example_data .groupby("symbol", group_keys=True) .apply(lambda x: roll_capm_estimation(x), include_groups=False) .reset_index() .drop(columns="level_1", errors="ignore"))# Filter to beta estimates onlybeta_examples = (capm_examples .query("coefficient == 'beta'") .merge(examples, on="symbol"))print(f"Rolling beta estimates: {len(beta_examples):,} observations")
Rolling beta estimates: 455 observations
15.5.4 Trực quan hóa beta trượt
Hình 22.7 thể hiện chuỗi thời gian các ước lượng beta của các cổ phiếu ví dụ. Hình cho thấy mức độ phơi nhiễm rủi ro hệ thống biến đổi khác nhau như thế nào giữa các ngành.
Hình 15.1: Ước lượng beta trượt theo tháng của một số cổ phiếu Việt Nam được chọn, dùng cửa sổ ước lượng 60 tháng. Các ngành khác nhau thể hiện những mô hình độ nhạy thị trường khác biệt theo thời gian.
Một số quy luật xuất hiện từ hình:
Khác biệt giữa các ngành: Cổ phiếu công nghệ và ngân hàng có thể thể hiện mô hình beta khác với doanh nghiệp bất động sản hay hàng tiêu dùng.
Biến thiên theo thời gian: Beta không cố định. Chúng phản ứng với những thay đổi của điều kiện kinh doanh, đòn bẩy và trạng thái thị trường.
Giai đoạn khủng hoảng: Các giai đoạn thị trường căng thẳng (ví dụ khủng hoảng tài chính 2008, COVID-19 năm 2020) thường làm các ước lượng beta thay đổi khi tương quan giữa các cổ phiếu tăng lên.
15.6 Ước lượng song song cho toàn thị trường
15.6.1 Thách thức về tính toán
Ước lượng beta trượt cho tất cả cổ phiếu trong cơ sở dữ liệu của chúng ta đòi hỏi nhiều tính toán. Với hàng trăm cổ phiếu, mỗi cổ phiếu cần ước lượng trượt qua nhiều kỳ, việc xử lý tuần tự sẽ mất khá nhiều thời gian. May mắn là việc ước lượng beta của các cổ phiếu khác nhau độc lập với nhau (tức là ước lượng của cổ phiếu A không phụ thuộc vào ước lượng của cổ phiếu B). Tính độc lập này khiến bài toán rất phù hợp để song song hóa.
15.6.2 Thiết lập xử lý song song
Chúng ta dùng thư viện joblib để phân phối tính toán trên nhiều nhân CPU. Lớp Parallel quản lý các tiến trình làm việc, còn delayed bọc các lời gọi hàm để thực thi trì hoãn.
Hiện mã
# Determine available cores (reserve one for system operations)n_cores =max(1, cpu_count() -1)print(f"Available cores for parallel processing: {n_cores}")
Available cores for parallel processing: 3
15.6.3 Ước lượng beta song song
Đoạn mã sau ước lượng beta trượt cho tất cả cổ phiếu một cách song song. Mỗi cổ phiếu được xử lý độc lập bởi một tiến trình làm việc riêng.
Hiện mã
def estimate_all_betas_parallel(data, n_cores, look_back=60, min_obs=48):""" Estimate rolling betas for all stocks using parallel processing. Parameters ---------- data : pd.DataFrame Full dataset with all stocks n_cores : int Number of CPU cores to use look_back : int Months in estimation window min_obs : int Minimum observations required Returns ------- pd.DataFrame Beta estimates for all stocks and dates """# Group data by stock grouped = data.groupby("symbol", group_keys=False)# Define worker functiondef process_stock(name, group): result = roll_capm_estimation(group, look_back=look_back, min_obs=min_obs)ifnot result.empty: result["symbol"] = namereturn result# Execute in parallel results = Parallel(n_jobs=n_cores, verbose=1)( delayed(process_stock)(name, group) for name, group in grouped )# Combine results results = [r for r in results ifnot r.empty]if results:return pd.concat(results, ignore_index=True)else:return pd.DataFrame()
Hiện mã
# Estimate betas for all stocksprint("Estimating rolling betas for all stocks...")capm_monthly = estimate_all_betas_parallel( prices_monthly, n_cores=n_cores, look_back=60, min_obs=48)print(f"\nCompleted: {len(capm_monthly):,} coefficient estimates")print(f"Unique stocks: {capm_monthly['symbol'].nunique():,}")
15.6.4 Lưu kết quả
Chúng ta lưu các ước lượng CAPM vào cơ sở dữ liệu để dùng trong các chương sau.
Hiện mã
capm_monthly.to_sql( name="capm_monthly", con=tidy_finance, if_exists="replace", index=False)print("CAPM estimates saved to database.")
Cho các phân tích tiếp theo, chúng ta nạp các ước lượng đã được tính sẵn:
Mặc dù lợi nhuận tháng là chuẩn mực trong nghiên cứu học thuật, một số ứng dụng được lợi từ dữ liệu có tần suất cao hơn:
Cửa sổ ước lượng ngắn hơn: Dữ liệu ngày cho phép ước lượng có ý nghĩa trong các giai đoạn ngắn hơn (ví dụ 3 tháng thay vì 5 năm).
Ước lượng phản ứng nhanh hơn: Beta ngày nắm bắt các thay đổi nhanh hơn.
Nghiên cứu sự kiện: Beta tần suất cao hữu ích để phân tích phản ứng của thị trường trước các sự kiện cụ thể.
Tuy nhiên, dữ liệu ngày kéo theo những thách thức bổ sung:
Nhiễu vi cấu trúc: Hiệu ứng nảy giá mua–bán và các ma sát giao dịch khác làm tăng nhiễu cho lợi nhuận.
Giao dịch không đồng bộ: Các cổ phiếu kém thanh khoản có thể không giao dịch mỗi ngày, làm ước lượng beta bị thiên lệch xuống.
Gánh nặng tính toán: Dữ liệu ngày lớn hơn dữ liệu tháng khoảng 21 lần.
15.7.1 Xử lý theo lô cho dữ liệu ngày
Do quy mô lớn của dữ liệu ngày, chúng ta xử lý cổ phiếu theo từng lô để kiểm soát giới hạn bộ nhớ. Cách tiếp cận này nạp và xử lý một tập con các cổ phiếu, lưu kết quả, rồi chuyển sang lô tiếp theo.
Hiện mã
def compute_market_return_daily(tidy_finance):""" Compute daily value-weighted market excess return from stock data. """# Load daily prices with market cap for weighting prices_daily_full = pd.read_sql_query( sql=""" SELECT p.symbol, p.date, p.ret_excess, m.mktcap_lag FROM prices_daily p LEFT JOIN prices_monthly m ON p.symbol = m.symbol AND strftime('%Y-%m', p.date) = strftime('%Y-%m', m.date) """, con=tidy_finance, parse_dates={"date"} )# Compute value-weighted market return each day mkt_daily = (prices_daily_full .dropna(subset=["ret_excess", "mktcap_lag"]) .groupby("date") .apply(lambda x: np.average(x["ret_excess"], weights=x["mktcap_lag"])) .reset_index(name="mkt_excess") )return mkt_dailydef roll_capm_estimation_daily(data, look_back_days=1260, min_obs=1000):""" Perform rolling-window CAPM estimation using daily data. Parameters ---------- data : pd.DataFrame DataFrame with 'date', 'ret_excess', and 'mkt_excess' columns look_back_days : int Number of trading days in the estimation window min_obs : int Minimum daily observations required within each window Returns ------- pd.DataFrame Time series of coefficient estimates with dates """ data = data.sort_values("date").copy() dates = data["date"].drop_duplicates().sort_values().reset_index(drop=True) results = []for i inrange(look_back_days -1, len(dates)): end_date = dates.iloc[i] start_idx =max(0, i - look_back_days +1) start_date = dates.iloc[start_idx] window_data = data.query("date >= @start_date and date <= @end_date") window_results = estimate_capm(window_data, min_obs=min_obs)ifnot window_results.empty: window_results["date"] = end_date results.append(window_results)if results:return pd.concat(results, ignore_index=True)else:return pd.DataFrame()def estimate_daily_betas_batch(symbols, tidy_finance, n_cores, batch_size=500, look_back_days=1260, min_obs=1000):""" Estimate rolling betas from daily data using batch processing. """# First, compute or load market returnprint("Computing daily market excess returns...") mkt_daily = compute_market_return_daily(tidy_finance)print(f"Market returns: {len(mkt_daily)} days") n_batches =int(np.ceil(len(symbols) / batch_size)) all_results = []for j inrange(n_batches): batch_start = j * batch_size batch_end =min((j +1) * batch_size, len(symbols)) batch_symbols = symbols[batch_start:batch_end] symbol_list =", ".join(f"'{s}'"for s in batch_symbols) query =f""" SELECT symbol, date, ret_excess FROM prices_daily WHERE symbol IN ({symbol_list}) """ prices_daily_batch = pd.read_sql_query( sql=query, con=tidy_finance, parse_dates={"date"} )# Merge with market excess return prices_daily_batch = prices_daily_batch.merge( mkt_daily, on="date", how="inner" )# Group by symbol and estimate betas grouped = prices_daily_batch.groupby("symbol", group_keys=False)# Parallel estimation batch_results = Parallel(n_jobs=n_cores)( delayed(lambda name, group: roll_capm_estimation_daily(group, look_back_days=look_back_days, min_obs=min_obs) .assign(symbol=name) )(name, group)for name, group in grouped ) batch_results = [r for r in batch_results if r isnotNoneandnot r.empty]if batch_results: all_results.append(pd.concat(batch_results, ignore_index=True))print(f"Batch {j+1}/{n_batches} complete")if all_results:return pd.concat(all_results, ignore_index=True)else:return pd.DataFrame()
Hiện mã
symbols = prices_monthly["symbol"].unique().tolist()capm_daily = estimate_daily_betas_batch( symbols=symbols, tidy_finance=tidy_finance, n_cores=n_cores, batch_size=500, look_back_days=1260, # ~5 years of trading days min_obs=1000)print(f"Daily beta estimates: {len(capm_daily):,}")
Hiện mã
capm_daily.to_sql( name="capm_daily", con=tidy_finance, if_exists="replace", index=False)print("CAPM estimates saved to database.")
Cho các phân tích tiếp theo, chúng ta nạp các ước lượng đã được tính sẵn:
Chúng ta xem xét phân phối của các ước lượng beta để kiểm tra tính hợp lý của chúng.
Hiện mã
print("Beta Summary Statistics:")print(beta_monthly["beta"].describe().round(3))# Additional diagnosticsprint(f"\nStocks with negative average beta: {(beta_monthly.groupby('symbol')['beta'].mean() <0).sum()}")print(f"Stocks with beta > 2: {(beta_monthly.groupby('symbol')['beta'].mean() >2).sum()}")
Beta Summary Statistics:
count 80790.000
mean 0.501
std 0.539
min -1.345
25% 0.130
50% 0.447
75% 0.832
max 2.678
Name: beta, dtype: float64
Stocks with negative average beta: 177
Stocks with beta > 2: 5
15.8.3 Phân phối beta giữa các ngành
Các ngành khác nhau có mức độ phơi nhiễm khác nhau trước rủi ro thị trường hệ thống, tùy theo mô hình kinh doanh, đòn bẩy hoạt động và đòn bẩy tài chính. Hình 15.2 thể hiện phân phối beta trung bình của từng doanh nghiệp giữa các ngành ở Việt Nam.
Hiện mã
# Merge betas with industry informationbeta_with_industry = (beta_monthly .merge( prices_monthly[["symbol", "date", "icb_name_vi"]].drop_duplicates(), on=["symbol", "date"], how="left" ) .dropna(subset=["icb_name_vi"]))# Compute firm-level average beta by industrybeta_by_industry = (beta_with_industry .groupby(["icb_name_vi", "symbol"])["beta"] .mean() .reset_index())# Order industries by median betaindustry_order = (beta_by_industry .groupby("icb_name_vi")["beta"] .median() .sort_values() .index.tolist())# Select top 10 industries by number of firms for clearer visualizationtop_industries = (beta_by_industry .groupby("icb_name_vi") .size() .nlargest(10) .index.tolist())beta_by_industry_filtered = beta_by_industry.query("icb_name_vi in @top_industries")
Hình 15.2: Phân phối beta trung bình cấp doanh nghiệp giữa các ngành ở Việt Nam. Biểu đồ hộp thể hiện trung vị, khoảng tứ phân vị và các giá trị ngoại lai của từng ngành.
15.8.4 Biến thiên theo thời gian của phân phối beta theo chiều ngang
Beta không chỉ khác nhau giữa các cổ phiếu mà còn thay đổi theo thời gian. Hình 15.3 cho thấy phân phối beta theo chiều ngang đã tiến triển như thế nào trên thị trường Việt Nam.
Hình 15.3: Các phân vị theo tháng của ước lượng beta theo thời gian. Mỗi đường biểu diễn một thập phân vị của phân phối beta theo chiều ngang.
Hình cho thấy một số quy luật đáng chú ý:
Dịch chuyển mức: Toàn bộ phân phối beta có thể dịch chuyển theo thời gian, phản ánh sự thay đổi của tương quan trên toàn thị trường.
Thay đổi độ phân tán: Trong giai đoạn thị trường căng thẳng, khoảng cách giữa các cổ phiếu beta cao và beta thấp có thể thay đổi khi các tương quan dịch chuyển.
Xu hướng: Một số giai đoạn cho thấy hành vi có xu hướng của beta, có thể phản ánh những thay đổi cấu trúc trong nền kinh tế.
15.8.5 Phân tích độ phủ
Chúng ta kiểm tra rằng quy trình ước lượng cho độ phủ hợp lý trên toàn mẫu. Hình 15.4 thể hiện tỷ lệ cổ phiếu có sẵn ước lượng beta theo thời gian.
Hiện mã
# Count stocks with and without betascoverage = (prices_monthly .groupby("date")["symbol"] .nunique() .reset_index(name="total_stocks") .merge( beta_monthly.groupby("date")["symbol"].nunique().reset_index(name="with_beta"), on="date", how="left" ) .fillna(0) .assign(coverage=lambda x: x["with_beta"] / x["total_stocks"]))coverage_figure = ( ggplot(coverage, aes(x="date", y="coverage"))+ geom_line(color="steelblue", size=1)+ labs( x="", y="Share with Beta Estimate", title="Beta Estimation Coverage Over Time" )+ scale_y_continuous(labels=percent_format(), limits=(0, 1))+ scale_x_datetime(date_breaks="2 years", date_labels="%Y")+ theme_minimal())coverage_figure.show()
Hình 15.4: Tỷ lệ cổ phiếu có sẵn ước lượng beta theo thời gian. Độ phủ tăng lên khi ngày càng nhiều cổ phiếu tích lũy đủ lịch sử lợi nhuận.
Độ phủ thấp hơn ở những năm đầu vì cổ phiếu cần có đủ lịch sử lợi nhuận (ít nhất 48 tháng) trước khi beta của chúng có thể được ước lượng. Khi thị trường trưởng thành và các cổ phiếu tích lũy lịch sử dài hơn, độ phủ tiến gần 100%.
15.9 So sánh ước lượng beta tháng và ngày
Khi cả ước lượng beta tháng và ngày đều sẵn có, chúng ta có thể so sánh chúng để hiểu tần suất ước lượng ảnh hưởng đến kết quả như thế nào.
# Filter to example stocksbeta_comparison = (beta_combined .merge(examples, on="symbol") .query("symbol in ['VIC', 'FPT']") # Select two for clarity)comparison_figure = ( ggplot( beta_comparison, aes(x="date", y="beta", color="frequency", linetype="frequency") )+ geom_line(size=0.8)+ facet_wrap("~company", ncol=1)+ labs( x="", y="Beta", color="Data Frequency", linetype="Data Frequency", title="Monthly vs Daily Beta Estimates" )+ scale_x_datetime(date_breaks="2 years", date_labels="%Y")+ theme_minimal()+ theme(legend_position="bottom"))comparison_figure.show()
Hình 15.5: So sánh ước lượng beta dùng lợi nhuận tháng với lợi nhuận ngày cho một số cổ phiếu được chọn. Ước lượng ngày mượt hơn do có nhiều quan sát hơn trong mỗi cửa sổ ước lượng.
Phép so sánh cho thấy các ước lượng dựa trên dữ liệu ngày nhìn chung mượt hơn nhờ số quan sát trong mỗi cửa sổ lớn hơn. Tuy nhiên, mức và xu hướng của các ước lượng tương tự nhau giữa hai tần suất, qua đó xác nhận rằng cả hai cách tiếp cận đều nắm bắt cùng một mức phơi nhiễm rủi ro hệ thống nền tảng.
Hiện mã
# Correlation between monthly and daily estimatescorrelation_data = (beta_combined .pivot_table(index=["symbol", "date"], columns="frequency", values="beta") .dropna())print(f"Correlation between monthly and daily betas: {correlation_data.corr().iloc[0,1]:.3f}")
Correlation between monthly and daily betas: 0.745
Bảng 15.1: Các lý do lý thuyết dẫn đến tương quan không hoàn hảo
Yếu tố
Tác động
Giao dịch không đồng bộ
Beta ngày có thể bị thiên lệch xuống đối với cổ phiếu kém thanh khoản
Nhiễu vi cấu trúc
Hiệu ứng nảy giá mua–bán làm tăng nhiễu cho ước lượng ngày
Cửa sổ hiệu dụng khác nhau
Cùng một giai đoạn lịch nhưng dữ liệu ngày có số quan sát nhiều hơn ~20 lần
Tốc độ hồi quy về trung bình
Dữ liệu ngày nắm bắt các động thái rủi ro biến đổi nhanh hơn
Bảng 15.1 cho thấy một số lý do khiến chúng ta có thể quan sát thấy tương quan không hoàn hảo.
15.10 Những điểm chính
Beta CAPM đo độ nhạy của một cổ phiếu trước rủi ro thị trường hệ thống và là nền tảng của lý thuyết danh mục đầu tư hiện đại, ước lượng chi phí vốn và quản trị rủi ro.
Ước lượng cửa sổ trượt nắm bắt sự biến thiên theo thời gian của beta, phản ánh những thay đổi trong mô hình kinh doanh, đòn bẩy của doanh nghiệp và điều kiện thị trường.
Song song hóa giảm đáng kể thời gian tính toán cho các tác vụ ước lượng quy mô lớn bằng cách phân chia công việc trên nhiều nhân CPU.
Các lựa chọn ước lượng đều quan trọng: Độ dài cửa sổ, tần suất lợi nhuận và yêu cầu về số quan sát tối thiểu đều ảnh hưởng đến các ước lượng beta. Nhà nghiên cứu nên chọn các tham số phù hợp với ứng dụng cụ thể của mình.
Quy luật theo ngành: Cổ phiếu Việt Nam cho thấy sự khác biệt có hệ thống về độ nhạy thị trường giữa các ngành, trong đó các ngành chu kỳ có beta cao hơn các ngành phòng thủ.
Biến thiên theo thời gian: Phân phối beta theo chiều ngang ở Việt Nam đã tiến triển theo thời gian, với những dịch chuyển đáng chú ý trong các giai đoạn thị trường căng thẳng.
So sánh tần suất: Các ước lượng beta tháng và ngày có tương quan dương nhưng không giống hệt nhau. Ước lượng ngày mượt hơn trong khi ước lượng tháng có thể nắm bắt tốt hơn các biến thiên tần suất thấp.
Kiểm tra chất lượng dữ liệu: Phân tích độ phủ và thống kê mô tả giúp phát hiện các vấn đề tiềm ẩn trong quy trình ước lượng trước khi dùng kết quả cho các phân tích tiếp theo.
---title: Ước lượng betaformat: html: toc: true number-sections: truejupyter: python3execute: echo: true warning: false message: false---Chương này giới thiệu một trong những khái niệm cơ bản nhất của kinh tế học tài chính: mức độ phơi nhiễm của một cổ phiếu riêng lẻ trước rủi ro thị trường mang tính hệ thống. Theo mô hình định giá tài sản vốn (CAPM) do @Sharpe1964, @Lintner1965 và @Mossin1966 phát triển, sự khác biệt theo chiều ngang trong lợi nhuận kỳ vọng của các tài sản phải được quyết định bởi hiệp phương sai giữa lợi nhuận vượt mức của tài sản và lợi nhuận vượt mức của danh mục thị trường. Hệ số hồi quy nắm bắt mối quan hệ này (thường được gọi là beta thị trường) là nền tảng của lý thuyết danh mục đầu tư hiện đại và vẫn được sử dụng rộng rãi trong thực tiễn để ước lượng chi phí vốn, phân tích đóng góp hiệu quả đầu tư và quản trị rủi ro.Trong chương này, chúng ta xây dựng một khung phân tích hoàn chỉnh để ước lượng beta thị trường cho các cổ phiếu Việt Nam. Chúng ta bắt đầu bằng một tổng quan khái niệm về CAPM và việc triển khai mô hình này trong thực nghiệm. Sau đó, chúng ta minh họa việc ước lượng beta bằng hồi quy bình phương nhỏ nhất thông thường, trước hết cho từng cổ phiếu riêng lẻ, rồi mở rộng ra toàn thị trường bằng ước lượng cửa sổ trượt. Để đáp ứng yêu cầu tính toán khi ước lượng beta cho hàng trăm cổ phiếu qua nhiều kỳ, chúng ta giới thiệu các kỹ thuật song song hóa giúp giảm đáng kể thời gian xử lý. Cuối cùng, chúng ta so sánh các ước lượng beta thu được từ lợi nhuận tháng với lợi nhuận ngày, và xem xét beta thay đổi như thế nào giữa các ngành và theo thời gian trên thị trường Việt Nam.Chương này vận dụng một số khái niệm tính toán quan trọng vượt ra ngoài phạm vi ước lượng beta. Ước lượng cửa sổ trượt là kỹ thuật có thể áp dụng cho mọi tham số biến thiên theo thời gian, còn song song hóa là giải pháp chung cho các tác vụ đòi hỏi nhiều tính toán nhưng có thể chia thành các tác vụ con độc lập.## Nền tảng lý thuyết### Mô hình định giá tài sản vốnCAPM cung cấp một khung lý thuyết liên kết lợi nhuận kỳ vọng với rủi ro hệ thống. Theo các giả định của mô hình, bao gồm nhà đầu tư tối ưu hóa trung bình–phương sai, kỳ vọng đồng nhất và thị trường không có ma sát, lợi nhuận vượt mức kỳ vọng của bất kỳ tài sản $i$ nào tỷ lệ thuận với hiệp phương sai của nó với danh mục thị trường:$$E[r_i - r_f] = \beta_i \cdot E[r_m - r_f]$$trong đó $r_i$ là lợi nhuận của tài sản $i$, $r_f$ là lãi suất phi rủi ro, $r_m$ là lợi nhuận của danh mục thị trường, và $\beta_i$ được định nghĩa như sau:$$\beta_i = \frac{\text{Cov}(r_i, r_m)}{\text{Var}(r_m)}$$Beta thị trường $\beta_i$ đo độ nhạy của lợi nhuận tài sản $i$ trước các biến động của thị trường. Beta lớn hơn một cho thấy tài sản khuếch đại các biến động của thị trường, trong khi beta nhỏ hơn một cho thấy độ nhạy thấp hơn. Beta bằng không hàm ý không có phơi nhiễm rủi ro hệ thống, chỉ còn lại rủi ro đặc thù có thể được triệt tiêu thông qua đa dạng hóa.### Triển khai trong thực nghiệmTrong thực tế, chúng ta ước lượng beta bằng cách hồi quy lợi nhuận vượt mức của cổ phiếu theo lợi nhuận vượt mức của thị trường:$$r_{i,t} - r_{f,t} = \alpha_i + \beta_i(r_{m,t} - r_{f,t}) + \varepsilon_{i,t}$$ {#eq-capm-regression}trong đó $\alpha_i$ biểu diễn lợi nhuận bất thường (alpha của Jensen), $\beta_i$ là beta thị trường cần ước lượng, và $\varepsilon_{i,t}$ là số hạng sai số đặc thù. Theo CAPM, $\alpha_i$ phải bằng không đối với mọi tài sản; bất kỳ alpha khác không nào cũng là sự lệch khỏi dự báo của mô hình.Một số cân nhắc thực tiễn ảnh hưởng đến việc ước lượng beta:1. **Cửa sổ ước lượng**: Cửa sổ dài hơn cho nhiều quan sát hơn và do đó ước lượng chính xác hơn, nhưng có thể chứa thông tin lỗi thời nếu beta thay đổi theo thời gian. Lựa chọn phổ biến nằm trong khoảng 36 đến 60 tháng đối với dữ liệu tháng.2. **Tần suất lợi nhuận**: Lợi nhuận tháng giảm nhiễu nhưng cho ít quan sát hơn. Lợi nhuận ngày cho nhiều điểm dữ liệu hơn nhưng có thể gây ra các hiệu ứng vi cấu trúc và thiên lệch do giao dịch không đồng bộ.3. **Đại diện thị trường**: Danh mục thị trường lý thuyết bao gồm mọi tài sản, nhưng trong thực tế chúng ta dùng một chỉ số cổ phiếu rộng. Đối với Việt Nam, chúng ta dùng lợi nhuận thị trường trọng số theo vốn hóa được xây dựng từ tập hợp cổ phiếu của chúng ta.4. **Số quan sát tối thiểu**: Yêu cầu một số quan sát tối thiểu (ví dụ 48 trên 60 tháng) giúp tránh các ước lượng thiếu tin cậy do dữ liệu thưa.## Thiết lập môi trườngChúng ta bắt đầu bằng việc nạp các gói Python cần thiết. Các gói cốt lõi đảm nhiệm xử lý dữ liệu, mô hình thống kê và các thao tác với cơ sở dữ liệu. Chúng ta cũng nhập các công cụ song song hóa, vốn sẽ rất cần thiết khi mở rộng việc ước lượng ra toàn thị trường.```{python}import pandas as pdimport numpy as npimport sqlite3import statsmodels.formula.api as smffrom scipy.stats.mstats import winsorizefrom plotnine import*from mizani.formatters import percent_format, comma_formatfrom joblib import Parallel, delayed, cpu_countfrom dateutil.relativedelta import relativedelta```Chúng ta kết nối với cơ sở dữ liệu SQLite chứa dữ liệu tài chính Việt Nam đã xử lý từ các chương trước.```{python}tidy_finance = sqlite3.connect(database="data/tidy_finance_python.sqlite")```## Nạp và chuẩn bị dữ liệu### Dữ liệu lợi nhuận cổ phiếuChúng ta nạp dữ liệu lợi nhuận cổ phiếu theo tháng đã được chuẩn bị trong chương Datacore. Dữ liệu bao gồm lợi nhuận vượt mức (lợi nhuận trừ đi lãi suất phi rủi ro) của tất cả cổ phiếu niêm yết tại Việt Nam.```{python}prices_monthly = pd.read_sql_query( sql=""" SELECT symbol, date, ret_excess FROM prices_monthly """, con=tidy_finance, parse_dates={"date"})# Add year for merging with fundamentalsprices_monthly["year"] = prices_monthly["date"].dt.yearprint(f"Loaded {len(prices_monthly):,} monthly observations")print(f"Covering {prices_monthly['symbol'].nunique():,} unique stocks")print(f"Date range: {prices_monthly['date'].min():%Y-%m} to {prices_monthly['date'].max():%Y-%m}")``````{python}prices_daily = pd.read_sql_query( sql=""" SELECT symbol, date, ret_excess FROM prices_daily """, con=tidy_finance, parse_dates={"date"})```### Thông tin doanh nghiệpChúng ta nạp thông tin doanh nghiệp để phục vụ phân tích các ước lượng beta ở cấp độ ngành.```{python}comp_vn = pd.read_sql_query( sql=""" SELECT symbol, datadate, icb_name_vi FROM comp_vn """, con=tidy_finance, parse_dates={"datadate"})# Extract year for mergingcomp_vn["year"] = comp_vn["datadate"].dt.yearprint(f"Company data: {comp_vn['symbol'].nunique():,} firms")```### Lợi nhuận vượt mức của thị trườngLàm đại diện cho danh mục thị trường, chúng ta dùng lợi nhuận vượt mức của thị trường theo trọng số vốn hóa. Nếu bạn đã xây dựng các nhân tố Fama-French ở một chương trước, hãy nạp chúng tại đây. Nếu không, chúng ta có thể xây dựng một lợi nhuận thị trường đơn giản từ dữ liệu cổ phiếu của mình.```{python}# Option 1: Load pre-computed market factorfactors_ff3_monthly = pd.read_sql_query( sql="SELECT date, mkt_excess FROM factors_ff3_monthly", con=tidy_finance, parse_dates={"date"})# Option 2: Construct market return from stock data (if factors not available)# This computes the value-weighted average return across all stocksdef compute_market_return(prices_df):""" Compute value-weighted market return from individual stock returns. Parameters ---------- prices_df : pd.DataFrame Stock returns with mktcap_lag for weighting Returns ------- pd.DataFrame Monthly market excess returns """ market_return = (prices_df .groupby("date") .apply(lambda x: np.average(x["ret_excess"], weights=x["mktcap_lag"])) .reset_index(name="mkt_excess") )return market_return```### Hợp nhất các bộ dữ liệuChúng ta kết hợp lợi nhuận cổ phiếu với lợi nhuận thị trường và thông tin doanh nghiệp để tạo ra bộ dữ liệu phục vụ ước lượng.```{python}# Merge stock returns with market returnsprices_monthly = prices_monthly.merge( factors_ff3_monthly, on="date", how="left")# Merge with company information for industry classificationprices_monthly = prices_monthly.merge( comp_vn[["symbol", "year", "icb_name_vi"]], on=["symbol", "year"], how="left")# Remove observations with missing dataprices_monthly = prices_monthly.dropna(subset=["ret_excess", "mkt_excess"])print(f"Final estimation sample: {len(prices_monthly):,} observations")```### Xử lý các giá trị ngoại laiCác mức lợi nhuận cực đoan có thể ảnh hưởng quá mức đến kết quả ước lượng hồi quy. Chúng ta áp dụng winsorization (cắt đuôi giá trị cực đoan) để hạn chế tác động của các giá trị ngoại lai trong khi vẫn giữ nguyên phân phối chung của lợi nhuận. Winsorization ở mức 1% thay các giá trị thấp hơn phân vị thứ 1 bằng giá trị phân vị thứ 1, và các giá trị cao hơn phân vị thứ 99 bằng giá trị phân vị thứ 99.```{python}def winsorize_returns(df, columns, limits=(0.01, 0.01)):""" Apply winsorization to return columns to limit outlier influence. Parameters ---------- df : pd.DataFrame DataFrame containing return columns columns : list Column names to winsorize limits : tuple Lower and upper percentile limits for winsorization Returns ------- pd.DataFrame DataFrame with winsorized columns """ df = df.copy()for col in columns: df[col] = winsorize(df[col], limits=limits)return dfprices_monthly = winsorize_returns( prices_monthly, columns=["ret_excess", "mkt_excess"], limits=(0.01, 0.01))print("Return distributions after winsorization:")print(prices_monthly[["ret_excess", "mkt_excess"]].describe().round(4))```## Ước lượng beta cho từng cổ phiếu### Ví dụ với một cổ phiếuTrước khi mở rộng ra toàn thị trường, chúng ta minh họa việc ước lượng beta cho một cổ phiếu nổi tiếng của Việt Nam. Chúng ta dùng Vingroup (VIC), một trong những tập đoàn lớn nhất Việt Nam với mức độ hiện diện đáng kể trong các lĩnh vực bất động sản, bán lẻ và ô tô.```{python}# Filter data for Vingroupvic_data = prices_monthly.query("symbol == 'VIC'").copy()print(f"VIC observations: {len(vic_data)}")print(f"Date range: {vic_data['date'].min():%Y-%m} to {vic_data['date'].max():%Y-%m}")```Chúng ta ước lượng hồi quy CAPM bằng phương pháp bình phương nhỏ nhất thông thường thông qua gói `statsmodels`. Giao diện công thức (formula) cung cấp một cách thuận tiện để chỉ định các mô hình hồi quy.```{python}# Estimate CAPM for Vingroupmodel_vic = smf.ols( formula="ret_excess ~ mkt_excess", data=vic_data).fit()# Display regression resultsprint(model_vic.summary())```Kết quả hồi quy cung cấp một số thông tin quan trọng:- **Beta (hệ số của mkt_excess)**: Độ nhạy thị trường ước lượng được. Beta lớn hơn 1 cho thấy VIC khuếch đại các biến động của thị trường.- **Alpha (Intercept)**: Lợi nhuận bất thường không được giải thích bởi mức độ phơi nhiễm thị trường. Theo CAPM, giá trị này phải bằng không.- **R-squared**: Tỷ lệ biến thiên của lợi nhuận được giải thích bởi các biến động của thị trường.- **Thống kê t**: Kiểm định xem các hệ số có khác không một cách có ý nghĩa hay không.```{python}# Extract key estimatescoefficients = model_vic.summary2().tables[1]print("\nKey estimates for Vingroup (VIC):")print(f" Beta: {coefficients.loc['mkt_excess', 'Coef.']:.3f}")print(f" Alpha: {coefficients.loc['Intercept', 'Coef.']:.4f}")print(f" R²: {model_vic.rsquared:.3f}")```### Hàm ước lượng CAPMChúng ta tạo một hàm có thể tái sử dụng, ước lượng CAPM và trả về kết quả theo định dạng chuẩn hóa. Hàm này bao gồm yêu cầu về số quan sát tối thiểu để tránh các ước lượng thiếu tin cậy do dữ liệu thưa.```{python}def estimate_capm(data, min_obs=48):""" Estimate CAPM regression and return coefficients. This function regresses excess stock returns on excess market returns and extracts the coefficient estimates along with t-statistics. Parameters ---------- data : pd.DataFrame DataFrame with 'ret_excess' and 'mkt_excess' columns min_obs : int Minimum number of observations required for estimation Returns ------- pd.DataFrame DataFrame with coefficient estimates and t-statistics, or empty DataFrame if insufficient observations """iflen(data) < min_obs:return pd.DataFrame()try:# Estimate OLS regression model = smf.ols( formula="ret_excess ~ mkt_excess", data=data ).fit()# Extract coefficient table coef_table = model.summary2().tables[1]# Format results results = pd.DataFrame({"coefficient": ["alpha", "beta"],"estimate": [ coef_table.loc["Intercept", "Coef."], coef_table.loc["mkt_excess", "Coef."] ],"t_statistic": [ coef_table.loc["Intercept", "t"], coef_table.loc["mkt_excess", "t"] ],"r_squared": model.rsquared })return resultsexceptExceptionas e:# Return empty DataFrame if estimation failsreturn pd.DataFrame()```## Ước lượng cửa sổ trượt### Động cơ của cửa sổ trượtBeta của cổ phiếu không cố định theo thời gian. Cơ cấu kinh doanh, đòn bẩy và môi trường hoạt động của doanh nghiệp thay đổi, khiến mức độ phơi nhiễm rủi ro hệ thống của nó cũng thay đổi. Để nắm bắt sự biến thiên theo thời gian này, chúng ta dùng ước lượng cửa sổ trượt: tại mỗi thời điểm, chúng ta ước lượng beta chỉ bằng dữ liệu từ một giai đoạn nhìn lại cố định (ví dụ 60 tháng gần nhất).Ước lượng cửa sổ trượt đòi hỏi một sự đánh đổi:- **Cửa sổ dài hơn** cho nhiều quan sát hơn và do đó ước lượng chính xác hơn, nhưng có thể chứa thông tin cũ.- **Cửa sổ ngắn hơn** phản ứng nhanh hơn với các thay đổi nhưng cho ước lượng nhiễu hơn.Một lựa chọn phổ biến trong nghiên cứu học thuật là 60 tháng (5 năm) dữ liệu tháng, đòi hỏi ít nhất 48 quan sát hợp lệ để ước lượng.### Triển khai cửa sổ trượtHàm sau triển khai ước lượng CAPM theo cửa sổ trượt. Với mỗi tháng trong mẫu, hàm nhìn lại trong cửa sổ đã chỉ định và ước lượng beta bằng toàn bộ dữ liệu sẵn có trong cửa sổ đó.```{python}def roll_capm_estimation(data, look_back=60, min_obs=48):""" Perform rolling-window CAPM estimation. This function slides a window across time, estimating the CAPM regression at each point using the most recent 'look_back' months of data. Parameters ---------- data : pd.DataFrame DataFrame with 'date', 'ret_excess', and 'mkt_excess' columns look_back : int Number of months in the estimation window min_obs : int Minimum observations required within each window Returns ------- pd.DataFrame Time series of coefficient estimates with dates """# Ensure data is sorted by date data = data.sort_values("date").copy()# Get unique dates dates = data["date"].drop_duplicates().sort_values()# Container for results results = []# Slide window across datesfor i inrange(look_back -1, len(dates)):# Define window boundaries end_date = dates.iloc[i] start_date = end_date - relativedelta(months=look_back -1)# Extract data within window window_data = data.query("date >= @start_date and date <= @end_date")# Estimate CAPM for this window window_results = estimate_capm(window_data, min_obs=min_obs)ifnot window_results.empty: window_results["date"] = end_date results.append(window_results)# Combine all resultsif results:return pd.concat(results, ignore_index=True)else:return pd.DataFrame()```### Ví dụ: beta trượt của một số cổ phiếu được chọnChúng ta minh họa ước lượng cửa sổ trượt cho một số cổ phiếu nổi tiếng của Việt Nam thuộc các ngành khác nhau.```{python}# Define example stocksexamples = pd.DataFrame({"symbol": ["FPT", "VNM", "VIC", "HPG", "VCB"],"company": ["FPT Corporation", # Technology"Vinamilk", # Consumer goods"Vingroup", # Real estate/conglomerate"Hoa Phat Group", # Steel/materials"Vietcombank"# Banking ]})# Check data availability for each exampledata_availability = (prices_monthly .query("symbol in @examples['symbol']") .groupby("symbol") .agg( n_obs=("date", "count"), first_date=("date", "min"), last_date=("date", "max") ) .reset_index())print("Data availability for example stocks:")print(data_availability)``````{python}# Estimate rolling betas for example stocksexample_data = prices_monthly.query("symbol in @examples['symbol']")capm_examples = (example_data .groupby("symbol", group_keys=True) .apply(lambda x: roll_capm_estimation(x), include_groups=False) .reset_index() .drop(columns="level_1", errors="ignore"))# Filter to beta estimates onlybeta_examples = (capm_examples .query("coefficient == 'beta'") .merge(examples, on="symbol"))print(f"Rolling beta estimates: {len(beta_examples):,} observations")```### Trực quan hóa beta trượt@fig-rolling-betas thể hiện chuỗi thời gian các ước lượng beta của các cổ phiếu ví dụ. Hình cho thấy mức độ phơi nhiễm rủi ro hệ thống biến đổi khác nhau như thế nào giữa các ngành.```{python}#| label: fig-rolling-betas#| out-width: "90%"#| fig-align: "center"#| fig-cap: "Ước lượng beta trượt theo tháng của một số cổ phiếu Việt Nam được chọn, dùng cửa sổ ước lượng 60 tháng. Các ngành khác nhau thể hiện những mô hình độ nhạy thị trường khác biệt theo thời gian."#| fig-alt: "Biểu đồ đường thể hiện chuỗi thời gian các ước lượng beta của năm cổ phiếu Việt Nam thuộc các ngành khác nhau."rolling_beta_figure = ( ggplot( beta_examples, aes(x="date", y="estimate", color="company") )+ geom_line(size=0.8)+ geom_hline(yintercept=1, linetype="dashed", color="gray", alpha=0.7)+ labs( x="", y="Beta", color="", title="Rolling Beta Estimates (60-Month Window)" )+ scale_x_datetime(date_breaks="2 years", date_labels="%Y")+ theme_minimal()+ theme(legend_position="bottom"))rolling_beta_figure.show()```Một số quy luật xuất hiện từ hình:1. **Khác biệt giữa các ngành**: Cổ phiếu công nghệ và ngân hàng có thể thể hiện mô hình beta khác với doanh nghiệp bất động sản hay hàng tiêu dùng.2. **Biến thiên theo thời gian**: Beta không cố định. Chúng phản ứng với những thay đổi của điều kiện kinh doanh, đòn bẩy và trạng thái thị trường.3. **Giai đoạn khủng hoảng**: Các giai đoạn thị trường căng thẳng (ví dụ khủng hoảng tài chính 2008, COVID-19 năm 2020) thường làm các ước lượng beta thay đổi khi tương quan giữa các cổ phiếu tăng lên.## Ước lượng song song cho toàn thị trường### Thách thức về tính toánƯớc lượng beta trượt cho tất cả cổ phiếu trong cơ sở dữ liệu của chúng ta đòi hỏi nhiều tính toán. Với hàng trăm cổ phiếu, mỗi cổ phiếu cần ước lượng trượt qua nhiều kỳ, việc xử lý tuần tự sẽ mất khá nhiều thời gian. May mắn là việc ước lượng beta của các cổ phiếu khác nhau độc lập với nhau (tức là ước lượng của cổ phiếu A không phụ thuộc vào ước lượng của cổ phiếu B). Tính độc lập này khiến bài toán rất phù hợp để song song hóa.### Thiết lập xử lý song songChúng ta dùng thư viện `joblib` để phân phối tính toán trên nhiều nhân CPU. Lớp `Parallel` quản lý các tiến trình làm việc, còn `delayed` bọc các lời gọi hàm để thực thi trì hoãn.```{python}# Determine available cores (reserve one for system operations)n_cores =max(1, cpu_count() -1)print(f"Available cores for parallel processing: {n_cores}")```### Ước lượng beta song songĐoạn mã sau ước lượng beta trượt cho tất cả cổ phiếu một cách song song. Mỗi cổ phiếu được xử lý độc lập bởi một tiến trình làm việc riêng.```{python}def estimate_all_betas_parallel(data, n_cores, look_back=60, min_obs=48):""" Estimate rolling betas for all stocks using parallel processing. Parameters ---------- data : pd.DataFrame Full dataset with all stocks n_cores : int Number of CPU cores to use look_back : int Months in estimation window min_obs : int Minimum observations required Returns ------- pd.DataFrame Beta estimates for all stocks and dates """# Group data by stock grouped = data.groupby("symbol", group_keys=False)# Define worker functiondef process_stock(name, group): result = roll_capm_estimation(group, look_back=look_back, min_obs=min_obs)ifnot result.empty: result["symbol"] = namereturn result# Execute in parallel results = Parallel(n_jobs=n_cores, verbose=1)( delayed(process_stock)(name, group) for name, group in grouped )# Combine results results = [r for r in results ifnot r.empty]if results:return pd.concat(results, ignore_index=True)else:return pd.DataFrame()``````{python}#| eval: false# Estimate betas for all stocksprint("Estimating rolling betas for all stocks...")capm_monthly = estimate_all_betas_parallel( prices_monthly, n_cores=n_cores, look_back=60, min_obs=48)print(f"\nCompleted: {len(capm_monthly):,} coefficient estimates")print(f"Unique stocks: {capm_monthly['symbol'].nunique():,}")```### Lưu kết quảChúng ta lưu các ước lượng CAPM vào cơ sở dữ liệu để dùng trong các chương sau.```{python}#| eval: falsecapm_monthly.to_sql( name="capm_monthly", con=tidy_finance, if_exists="replace", index=False)print("CAPM estimates saved to database.")```Cho các phân tích tiếp theo, chúng ta nạp các ước lượng đã được tính sẵn:```{python}capm_monthly = pd.read_sql_query( sql="SELECT * FROM capm_monthly", con=tidy_finance, parse_dates={"date"})print(f"Loaded {len(capm_monthly):,} CAPM estimates")```## Ước lượng beta bằng lợi nhuận ngàyMặc dù lợi nhuận tháng là chuẩn mực trong nghiên cứu học thuật, một số ứng dụng được lợi từ dữ liệu có tần suất cao hơn:- **Cửa sổ ước lượng ngắn hơn**: Dữ liệu ngày cho phép ước lượng có ý nghĩa trong các giai đoạn ngắn hơn (ví dụ 3 tháng thay vì 5 năm).- **Ước lượng phản ứng nhanh hơn**: Beta ngày nắm bắt các thay đổi nhanh hơn.- **Nghiên cứu sự kiện**: Beta tần suất cao hữu ích để phân tích phản ứng của thị trường trước các sự kiện cụ thể.Tuy nhiên, dữ liệu ngày kéo theo những thách thức bổ sung:- **Nhiễu vi cấu trúc**: Hiệu ứng nảy giá mua–bán và các ma sát giao dịch khác làm tăng nhiễu cho lợi nhuận.- **Giao dịch không đồng bộ**: Các cổ phiếu kém thanh khoản có thể không giao dịch mỗi ngày, làm ước lượng beta bị thiên lệch xuống.- **Gánh nặng tính toán**: Dữ liệu ngày lớn hơn dữ liệu tháng khoảng 21 lần.### Xử lý theo lô cho dữ liệu ngàyDo quy mô lớn của dữ liệu ngày, chúng ta xử lý cổ phiếu theo từng lô để kiểm soát giới hạn bộ nhớ. Cách tiếp cận này nạp và xử lý một tập con các cổ phiếu, lưu kết quả, rồi chuyển sang lô tiếp theo.```{python}def compute_market_return_daily(tidy_finance):""" Compute daily value-weighted market excess return from stock data. """# Load daily prices with market cap for weighting prices_daily_full = pd.read_sql_query( sql=""" SELECT p.symbol, p.date, p.ret_excess, m.mktcap_lag FROM prices_daily p LEFT JOIN prices_monthly m ON p.symbol = m.symbol AND strftime('%Y-%m', p.date) = strftime('%Y-%m', m.date) """, con=tidy_finance, parse_dates={"date"} )# Compute value-weighted market return each day mkt_daily = (prices_daily_full .dropna(subset=["ret_excess", "mktcap_lag"]) .groupby("date") .apply(lambda x: np.average(x["ret_excess"], weights=x["mktcap_lag"])) .reset_index(name="mkt_excess") )return mkt_dailydef roll_capm_estimation_daily(data, look_back_days=1260, min_obs=1000):""" Perform rolling-window CAPM estimation using daily data. Parameters ---------- data : pd.DataFrame DataFrame with 'date', 'ret_excess', and 'mkt_excess' columns look_back_days : int Number of trading days in the estimation window min_obs : int Minimum daily observations required within each window Returns ------- pd.DataFrame Time series of coefficient estimates with dates """ data = data.sort_values("date").copy() dates = data["date"].drop_duplicates().sort_values().reset_index(drop=True) results = []for i inrange(look_back_days -1, len(dates)): end_date = dates.iloc[i] start_idx =max(0, i - look_back_days +1) start_date = dates.iloc[start_idx] window_data = data.query("date >= @start_date and date <= @end_date") window_results = estimate_capm(window_data, min_obs=min_obs)ifnot window_results.empty: window_results["date"] = end_date results.append(window_results)if results:return pd.concat(results, ignore_index=True)else:return pd.DataFrame()def estimate_daily_betas_batch(symbols, tidy_finance, n_cores, batch_size=500, look_back_days=1260, min_obs=1000):""" Estimate rolling betas from daily data using batch processing. """# First, compute or load market returnprint("Computing daily market excess returns...") mkt_daily = compute_market_return_daily(tidy_finance)print(f"Market returns: {len(mkt_daily)} days") n_batches =int(np.ceil(len(symbols) / batch_size)) all_results = []for j inrange(n_batches): batch_start = j * batch_size batch_end =min((j +1) * batch_size, len(symbols)) batch_symbols = symbols[batch_start:batch_end] symbol_list =", ".join(f"'{s}'"for s in batch_symbols) query =f""" SELECT symbol, date, ret_excess FROM prices_daily WHERE symbol IN ({symbol_list}) """ prices_daily_batch = pd.read_sql_query( sql=query, con=tidy_finance, parse_dates={"date"} )# Merge with market excess return prices_daily_batch = prices_daily_batch.merge( mkt_daily, on="date", how="inner" )# Group by symbol and estimate betas grouped = prices_daily_batch.groupby("symbol", group_keys=False)# Parallel estimation batch_results = Parallel(n_jobs=n_cores)( delayed(lambda name, group: roll_capm_estimation_daily(group, look_back_days=look_back_days, min_obs=min_obs) .assign(symbol=name) )(name, group)for name, group in grouped ) batch_results = [r for r in batch_results if r isnotNoneandnot r.empty]if batch_results: all_results.append(pd.concat(batch_results, ignore_index=True))print(f"Batch {j+1}/{n_batches} complete")if all_results:return pd.concat(all_results, ignore_index=True)else:return pd.DataFrame()``````{python}#| eval: falsesymbols = prices_monthly["symbol"].unique().tolist()capm_daily = estimate_daily_betas_batch( symbols=symbols, tidy_finance=tidy_finance, n_cores=n_cores, batch_size=500, look_back_days=1260, # ~5 years of trading days min_obs=1000)print(f"Daily beta estimates: {len(capm_daily):,}")``````{python}#| eval: falsecapm_daily.to_sql( name="capm_daily", con=tidy_finance, if_exists="replace", index=False)print("CAPM estimates saved to database.")```Cho các phân tích tiếp theo, chúng ta nạp các ước lượng đã được tính sẵn:```{python}capm_daily = pd.read_sql_query( sql="SELECT * FROM capm_daily", con=tidy_finance, parse_dates={"date"})print(f"Loaded {len(capm_daily):,} CAPM estimates")```## Phân tích các ước lượng beta### Trích xuất các ước lượng betaChúng ta trích xuất các hệ số beta ước lượng từ kết quả CAPM để phân tích.```{python}# Extract monthly betasbeta_monthly = (capm_monthly .query("coefficient == 'beta'") .rename(columns={"estimate": "beta"}) [["symbol", "date", "beta"]] .assign(frequency="monthly"))# Save to databasebeta_monthly.to_sql( name="beta_monthly", con=tidy_finance, if_exists="replace", index=False)print(f"Monthly betas: {len(beta_monthly):,} observations")print(f"Unique stocks: {beta_monthly['symbol'].nunique():,}")``````{python}# Load pre-computed betasbeta_monthly = pd.read_sql_query( sql="SELECT * FROM beta_monthly", con=tidy_finance, parse_dates={"date"})```### Thống kê mô tảChúng ta xem xét phân phối của các ước lượng beta để kiểm tra tính hợp lý của chúng.```{python}print("Beta Summary Statistics:")print(beta_monthly["beta"].describe().round(3))# Additional diagnosticsprint(f"\nStocks with negative average beta: {(beta_monthly.groupby('symbol')['beta'].mean() <0).sum()}")print(f"Stocks with beta > 2: {(beta_monthly.groupby('symbol')['beta'].mean() >2).sum()}")```### Phân phối beta giữa các ngànhCác ngành khác nhau có mức độ phơi nhiễm khác nhau trước rủi ro thị trường hệ thống, tùy theo mô hình kinh doanh, đòn bẩy hoạt động và đòn bẩy tài chính. @fig-beta-by-industry thể hiện phân phối beta trung bình của từng doanh nghiệp giữa các ngành ở Việt Nam.```{python}# Merge betas with industry informationbeta_with_industry = (beta_monthly .merge( prices_monthly[["symbol", "date", "icb_name_vi"]].drop_duplicates(), on=["symbol", "date"], how="left" ) .dropna(subset=["icb_name_vi"]))# Compute firm-level average beta by industrybeta_by_industry = (beta_with_industry .groupby(["icb_name_vi", "symbol"])["beta"] .mean() .reset_index())# Order industries by median betaindustry_order = (beta_by_industry .groupby("icb_name_vi")["beta"] .median() .sort_values() .index.tolist())# Select top 10 industries by number of firms for clearer visualizationtop_industries = (beta_by_industry .groupby("icb_name_vi") .size() .nlargest(10) .index.tolist())beta_by_industry_filtered = beta_by_industry.query("icb_name_vi in @top_industries")``````{python}#| label: fig-beta-by-industry#| fig-cap: "Phân phối beta trung bình cấp doanh nghiệp giữa các ngành ở Việt Nam. Biểu đồ hộp thể hiện trung vị, khoảng tứ phân vị và các giá trị ngoại lai của từng ngành."#| fig-alt: "Biểu đồ hộp thể hiện phân phối beta theo ngành, sắp xếp theo beta trung vị."beta_industry_figure = ( ggplot( beta_by_industry_filtered, aes(x="icb_name_vi", y="beta") )+ geom_boxplot(fill="steelblue", alpha=0.7)+ geom_hline(yintercept=1, linetype="dashed", color="red", alpha=0.7)+ coord_flip()+ labs( x="", y="Beta", title="Beta Distribution by Industry" )+ theme_minimal())beta_industry_figure.show()```### Biến thiên theo thời gian của phân phối beta theo chiều ngangBeta không chỉ khác nhau giữa các cổ phiếu mà còn thay đổi theo thời gian. @fig-beta-quantiles cho thấy phân phối beta theo chiều ngang đã tiến triển như thế nào trên thị trường Việt Nam.```{python}#| label: fig-beta-quantiles#| fig-cap: "Các phân vị theo tháng của ước lượng beta theo thời gian. Mỗi đường biểu diễn một thập phân vị của phân phối beta theo chiều ngang."#| fig-alt: "Biểu đồ đường thể hiện chuỗi thời gian các thập phân vị beta, minh họa phân phối beta đã thay đổi như thế nào theo thời gian."# Compute monthly quantilesbeta_quantiles = (beta_monthly .groupby("date")["beta"] .quantile(q=np.arange(0.1, 1.0, 0.1)) .reset_index() .rename(columns={"level_1": "quantile"}) .assign(quantile=lambda x: (x["quantile"] *100).astype(int).astype(str) +"%"))beta_quantiles_figure = ( ggplot( beta_quantiles, aes(x="date", y="beta", color="quantile") )+ geom_line(alpha=0.8)+ geom_hline(yintercept=1, linetype="dashed", color="gray")+ labs( x="", y="Beta", color="Quantile", title="Cross-Sectional Distribution of Betas Over Time" )+ scale_x_datetime(date_breaks="2 years", date_labels="%Y")+ theme_minimal())beta_quantiles_figure.show()```Hình cho thấy một số quy luật đáng chú ý:1. **Dịch chuyển mức**: Toàn bộ phân phối beta có thể dịch chuyển theo thời gian, phản ánh sự thay đổi của tương quan trên toàn thị trường.2. **Thay đổi độ phân tán**: Trong giai đoạn thị trường căng thẳng, khoảng cách giữa các cổ phiếu beta cao và beta thấp có thể thay đổi khi các tương quan dịch chuyển.3. **Xu hướng**: Một số giai đoạn cho thấy hành vi có xu hướng của beta, có thể phản ánh những thay đổi cấu trúc trong nền kinh tế.### Phân tích độ phủChúng ta kiểm tra rằng quy trình ước lượng cho độ phủ hợp lý trên toàn mẫu. @fig-beta-coverage thể hiện tỷ lệ cổ phiếu có sẵn ước lượng beta theo thời gian.```{python}#| label: fig-beta-coverage#| fig-cap: "Tỷ lệ cổ phiếu có sẵn ước lượng beta theo thời gian. Độ phủ tăng lên khi ngày càng nhiều cổ phiếu tích lũy đủ lịch sử lợi nhuận."#| fig-alt: "Biểu đồ đường thể hiện phần trăm cổ phiếu có sẵn ước lượng beta mỗi tháng."# Count stocks with and without betascoverage = (prices_monthly .groupby("date")["symbol"] .nunique() .reset_index(name="total_stocks") .merge( beta_monthly.groupby("date")["symbol"].nunique().reset_index(name="with_beta"), on="date", how="left" ) .fillna(0) .assign(coverage=lambda x: x["with_beta"] / x["total_stocks"]))coverage_figure = ( ggplot(coverage, aes(x="date", y="coverage"))+ geom_line(color="steelblue", size=1)+ labs( x="", y="Share with Beta Estimate", title="Beta Estimation Coverage Over Time" )+ scale_y_continuous(labels=percent_format(), limits=(0, 1))+ scale_x_datetime(date_breaks="2 years", date_labels="%Y")+ theme_minimal())coverage_figure.show()```Độ phủ thấp hơn ở những năm đầu vì cổ phiếu cần có đủ lịch sử lợi nhuận (ít nhất 48 tháng) trước khi beta của chúng có thể được ước lượng. Khi thị trường trưởng thành và các cổ phiếu tích lũy lịch sử dài hơn, độ phủ tiến gần 100%.## So sánh ước lượng beta tháng và ngàyKhi cả ước lượng beta tháng và ngày đều sẵn có, chúng ta có thể so sánh chúng để hiểu tần suất ước lượng ảnh hưởng đến kết quả như thế nào.```{python}# Combine monthly and daily estimatesbeta_daily = (capm_daily .query("coefficient == 'beta'") .rename(columns={"estimate": "beta"}) [["symbol", "date", "beta"]] .assign(frequency="daily"))beta_combined = pd.concat([beta_monthly, beta_daily], ignore_index=True)``````{python}#| label: fig-beta-comparison#| fig-cap: "So sánh ước lượng beta dùng lợi nhuận tháng với lợi nhuận ngày cho một số cổ phiếu được chọn. Ước lượng ngày mượt hơn do có nhiều quan sát hơn trong mỗi cửa sổ ước lượng."#| fig-alt: "Biểu đồ đường so sánh ước lượng beta tháng và ngày theo thời gian cho các cổ phiếu ví dụ."# Filter to example stocksbeta_comparison = (beta_combined .merge(examples, on="symbol") .query("symbol in ['VIC', 'FPT']") # Select two for clarity)comparison_figure = ( ggplot( beta_comparison, aes(x="date", y="beta", color="frequency", linetype="frequency") )+ geom_line(size=0.8)+ facet_wrap("~company", ncol=1)+ labs( x="", y="Beta", color="Data Frequency", linetype="Data Frequency", title="Monthly vs Daily Beta Estimates" )+ scale_x_datetime(date_breaks="2 years", date_labels="%Y")+ theme_minimal()+ theme(legend_position="bottom"))comparison_figure.show()```Phép so sánh cho thấy các ước lượng dựa trên dữ liệu ngày nhìn chung mượt hơn nhờ số quan sát trong mỗi cửa sổ lớn hơn. Tuy nhiên, mức và xu hướng của các ước lượng tương tự nhau giữa hai tần suất, qua đó xác nhận rằng cả hai cách tiếp cận đều nắm bắt cùng một mức phơi nhiễm rủi ro hệ thống nền tảng.```{python}# Correlation between monthly and daily estimatescorrelation_data = (beta_combined .pivot_table(index=["symbol", "date"], columns="frequency", values="beta") .dropna())print(f"Correlation between monthly and daily betas: {correlation_data.corr().iloc[0,1]:.3f}")```| Yếu tố | Tác động ||------------------------------------|------------------------------------|| Giao dịch không đồng bộ | Beta ngày có thể bị thiên lệch xuống đối với cổ phiếu kém thanh khoản || Nhiễu vi cấu trúc | Hiệu ứng nảy giá mua–bán làm tăng nhiễu cho ước lượng ngày || Cửa sổ hiệu dụng khác nhau | Cùng một giai đoạn lịch nhưng dữ liệu ngày có số quan sát nhiều hơn \~20 lần || Tốc độ hồi quy về trung bình | Dữ liệu ngày nắm bắt các động thái rủi ro biến đổi nhanh hơn |: Các lý do lý thuyết dẫn đến tương quan không hoàn hảo {#tbl-imperfect-cor}@tbl-imperfect-cor cho thấy một số lý do khiến chúng ta có thể quan sát thấy tương quan không hoàn hảo.## Những điểm chính1. **Beta CAPM** đo độ nhạy của một cổ phiếu trước rủi ro thị trường hệ thống và là nền tảng của lý thuyết danh mục đầu tư hiện đại, ước lượng chi phí vốn và quản trị rủi ro.2. **Ước lượng cửa sổ trượt** nắm bắt sự biến thiên theo thời gian của beta, phản ánh những thay đổi trong mô hình kinh doanh, đòn bẩy của doanh nghiệp và điều kiện thị trường.3. **Song song hóa** giảm đáng kể thời gian tính toán cho các tác vụ ước lượng quy mô lớn bằng cách phân chia công việc trên nhiều nhân CPU.4. **Các lựa chọn ước lượng đều quan trọng**: Độ dài cửa sổ, tần suất lợi nhuận và yêu cầu về số quan sát tối thiểu đều ảnh hưởng đến các ước lượng beta. Nhà nghiên cứu nên chọn các tham số phù hợp với ứng dụng cụ thể của mình.5. **Quy luật theo ngành**: Cổ phiếu Việt Nam cho thấy sự khác biệt có hệ thống về độ nhạy thị trường giữa các ngành, trong đó các ngành chu kỳ có beta cao hơn các ngành phòng thủ.6. **Biến thiên theo thời gian**: Phân phối beta theo chiều ngang ở Việt Nam đã tiến triển theo thời gian, với những dịch chuyển đáng chú ý trong các giai đoạn thị trường căng thẳng.7. **So sánh tần suất**: Các ước lượng beta tháng và ngày có tương quan dương nhưng không giống hệt nhau. Ước lượng ngày mượt hơn trong khi ước lượng tháng có thể nắm bắt tốt hơn các biến thiên tần suất thấp.8. **Kiểm tra chất lượng dữ liệu**: Phân tích độ phủ và thống kê mô tả giúp phát hiện các vấn đề tiềm ẩn trong quy trình ước lượng trước khi dùng kết quả cho các phân tích tiếp theo.