Chương này trình bày việc tái lập các danh mục nhân tố Fama-French cho thị trường chứng khoán Việt Nam. Các mô hình nhân tố Fama-French là nền tảng của định giá tài sản thực nghiệm, bắt nguồn từ công trình kinh điển của Fama và French (1992) và sau đó được mở rộng trong Fama và French (2015). Các mô hình này đã thay đổi cách giới học thuật và nhà thực hành hiểu về sự khác biệt theo chiều ngang của lợi nhuận kỳ vọng cổ phiếu, vượt ra ngoài mô hình định giá tài sản vốn một nhân tố để đưa vào nhiều nguồn rủi ro hệ thống.
Chúng ta xây dựng cả mô hình ba nhân tố và năm nhân tố ở tần suất tháng và ngày. Các nhân tố tháng là nền tảng cho hầu hết các kiểm định định giá tài sản và phân tích danh mục, trong khi các nhân tố ngày cho phép các ứng dụng tần suất cao hơn, bao gồm nghiên cứu sự kiện ngắn hạn, nghiên cứu vi cấu trúc thị trường và ước lượng beta ngày. Bằng cách xây dựng nhân tố ở cả hai tần suất, chúng ta tạo ra một bộ công cụ hoàn chỉnh cho nghiên cứu tài chính thực nghiệm trên thị trường Việt Nam.
Chương được triển khai như sau. Trước hết, chúng ta bàn về động cơ lý thuyết của từng nhân tố và trực giác kinh tế đằng sau phương pháp Fama-French. Sau đó, chúng ta chuẩn bị dữ liệu cần thiết, hợp nhất lợi nhuận cổ phiếu với các đặc điểm kế toán. Tiếp theo, chúng ta triển khai các quy trình phân loại danh mục làm cơ sở cho việc xây dựng nhân tố, bám sát các quy trình Fama-French gốc đồng thời điều chỉnh cho phù hợp với đặc điểm thị trường Việt Nam. Chúng ta xây dựng mô hình ba nhân tố (thị trường, quy mô và giá trị) trước khi mở rộng sang mô hình năm nhân tố (bổ sung khả năng sinh lời và đầu tư). Cuối cùng, chúng ta xây dựng các nhân tố ngày và kiểm chứng các nhân tố đã tái lập thông qua nhiều kiểm tra chẩn đoán.
17.1 Cơ sở lý thuyết
17.1.1 Quá trình phát triển từ CAPM đến các mô hình đa nhân tố
Mô hình định giá tài sản vốn của Sharpe (1964) cho rằng một nhân tố duy nhất, danh mục thị trường, phải giải thích toàn bộ sự khác biệt theo chiều ngang của lợi nhuận kỳ vọng. Tuy nhiên, nhiều thập kỷ nghiên cứu thực nghiệm đã ghi nhận những quy luật dai dẳng mà CAPM không giải thích được. Fama và French (1992) đã chứng minh rằng hai đặc điểm của doanh nghiệp, quy mô và tỷ số giá trị sổ sách trên giá trị thị trường, nắm bắt một phần đáng kể biến thiên của lợi nhuận trung bình mà beta thị trường không giải thích được.
Các doanh nghiệp nhỏ có xu hướng kiếm được lợi nhuận cao hơn các doanh nghiệp lớn, một quy luật được gọi là hiệu ứng quy mô. Tương tự, các doanh nghiệp có tỷ số giá trị sổ sách trên giá trị thị trường cao (cổ phiếu giá trị) có xu hướng vượt trội các doanh nghiệp có tỷ số này thấp (cổ phiếu tăng trưởng), được gọi là phần bù giá trị. Mô hình ba nhân tố chính thức hóa các quan sát này bằng cách xây dựng các danh mục nhân tố có thể giao dịch được:
\(r_{i,t} - r_{f,t}\) là lợi nhuận vượt mức của tài sản \(i\)
\(r_{m,t} - r_{f,t}\) là lợi nhuận vượt mức của thị trường
\(SMB_t\) (Small Minus Big) là nhân tố quy mô
\(HML_t\) (High Minus Low) là nhân tố giá trị
17.1.2 Mở rộng sang năm nhân tố
Fama và French (2015) mở rộng mô hình để bao gồm hai nhân tố bổ sung, được gợi ý từ mô hình chiết khấu cổ tức. Các doanh nghiệp có khả năng sinh lời cao hơn phải có lợi nhuận kỳ vọng cao hơn (các yếu tố khác không đổi), và các doanh nghiệp có chính sách đầu tư mạnh tay phải có lợi nhuận kỳ vọng thấp hơn:
\(RMW_t\) (Robust Minus Weak) là nhân tố khả năng sinh lời
\(CMA_t\) (Conservative Minus Aggressive) là nhân tố đầu tư
17.1.3 Phương pháp xây dựng nhân tố
Phương pháp Fama-French xây dựng các nhân tố thông qua các danh mục phân loại kép:
Phân loại theo quy mô: Cổ phiếu được chia thành nhóm Nhỏ và Lớn dựa trên vốn hóa thị trường trung vị.
Phân loại theo đặc điểm: Trong mỗi nhóm quy mô, cổ phiếu được phân loại thành ba nhóm theo tam phân vị dựa trên tỷ số giá trị sổ sách trên giá trị thị trường (cho HML), khả năng sinh lời hoạt động (cho RMW) hoặc đầu tư (cho CMA).
Lợi nhuận nhân tố: Các nhân tố được tính bằng chênh lệch giữa lợi nhuận trung bình của các danh mục có giá trị đặc điểm cao so với thấp, lấy trung bình qua các nhóm quy mô để loại bỏ hiệu ứng quy mô.
Thời điểm: Các danh mục được hình thành vào cuối tháng Sáu mỗi năm, dùng dữ liệu kế toán của năm tài chính trước đó, bảo đảm toàn bộ thông tin đã được công bố công khai tại thời điểm hình thành.
17.2 Thiết lập môi trường
Chúng ta nạp các gói Python cần thiết để xử lý dữ liệu, phân tích thống kê và trực quan hóa.
Hiện mã
import pandas as pdimport numpy as npimport sqlite3import statsmodels.formula.api as smffrom scipy.stats.mstats import winsorizeimport matplotlib.pyplot as pltfrom plotnine import*from mizani.formatters import percent_format, comma_format
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ý.
Chúng ta nạp dữ liệu lợi nhuận cổ phiếu theo tháng, bao gồm lợi nhuận vượt mức, vốn hóa thị trường và lãi suất phi rủi ro. Các biến này là thiết yếu để tính lợi nhuận danh mục theo trọng số vốn hóa và phần bù nhân tố.
Monthly returns: 165,499 observations
Unique stocks: 1,457
Date range: 2010-02 to 2023-12
17.3.2 Nạp dữ liệu cơ bản của doanh nghiệp
Chúng ta nạp dữ liệu cơ bản của doanh nghiệp gồm vốn chủ sở hữu sổ sách, khả năng sinh lời hoạt động và đầu tư, tức là các đặc điểm cần thiết để xây dựng các nhân tố Fama-French.
Hiện mã
comp_vn = pd.read_sql_query( sql=""" SELECT symbol, datadate, be, op, inv FROM comp_vn """, con=tidy_finance, parse_dates={"datadate"}).dropna()print(f"Fundamentals: {len(comp_vn):,} firm-year observations")print(f"Unique firms: {comp_vn['symbol'].nunique():,}")
Theo quy ước Fama-French, chúng ta xây dựng các biến phân loại với sự chú ý cẩn thận đến thời điểm. Các nguyên tắc chính là:
Quy mô (vốn hóa tháng Sáu): Chúng ta dùng vốn hóa thị trường vào cuối tháng Sáu của năm \(t\) để phân loại cổ phiếu vào các nhóm quy mô. Điều này bảo đảm chúng ta nắm bắt quy mô của doanh nghiệp vào đúng thời điểm hình thành danh mục.
Tỷ số giá trị sổ sách trên giá trị thị trường: Chúng ta dùng vốn chủ sở hữu sổ sách của năm tài chính \(t-1\) chia cho vốn hóa thị trường vào cuối tháng Mười Hai \(t-1\). Điều này tạo ra độ trễ sáu tháng giữa dữ liệu kế toán và thời điểm hình thành danh mục, bảo đảm thông tin đã được công bố công khai.
Ngày hình thành danh mục: Các danh mục được hình thành vào ngày 1 tháng Bảy và được nắm giữ mười hai tháng cho đến tháng Sáu năm sau.
Hiện mã
def construct_sorting_variables(prices_monthly, comp_vn):""" Construct sorting variables following Fama-French methodology. Parameters ---------- prices_monthly : pd.DataFrame Monthly stock returns with market cap comp_vn : pd.DataFrame Company fundamentals with book equity, profitability, investment Returns ------- pd.DataFrame Sorting variables aligned with July 1st formation dates """# 1. Size: June market capitalization# Portfolio formation is July 1st, so we use June market cap size = (prices_monthly .query("date.dt.month == 6") .assign( sorting_date=lambda x: x["date"] + pd.offsets.MonthBegin(1) ) [["symbol", "sorting_date", "mktcap"]] .rename(columns={"mktcap": "size"}) )print(f"Size observations: {len(size):,}")# 2. Market Equity: December market cap for B/M calculation# December t-1 market cap is used with fiscal year t-1 book equity# This is then used for July t portfolio formation market_equity = (prices_monthly .query("date.dt.month == 12") .assign(# December year t-1 maps to July year t formation sorting_date=lambda x: x["date"] + pd.offsets.MonthBegin(7) ) [["symbol", "sorting_date", "mktcap"]] .rename(columns={"mktcap": "me"}) )print(f"Market equity observations: {len(market_equity):,}")# 3. Book-to-Market and other characteristics# Fiscal year t-1 data is used for July t portfolio formation book_to_market = (comp_vn .assign(# Fiscal year-end + 6 months = July formation sorting_date=lambda x: pd.to_datetime( (x["datadate"].dt.year +1).astype(str) +"-07-01" ) ) .merge(market_equity, on=["symbol", "sorting_date"], how="inner") .assign(# Scale book equity to match market equity units# BE is in VND, ME is in millions VND bm=lambda x: x["be"] / (x["me"] *1e9) ) [["symbol", "sorting_date", "me", "bm", "op", "inv"]] )print(f"Book-to-market observations: {len(book_to_market):,}")# 4. Merge size with characteristics sorting_variables = (size .merge(book_to_market, on=["symbol", "sorting_date"], how="inner") .dropna() .drop_duplicates(subset=["symbol", "sorting_date"]) )return sorting_variablessorting_variables = construct_sorting_variables(prices_monthly, comp_vn)print(f"\nFinal sorting variables: {len(sorting_variables):,} stock-years")print(f"Sorting date range: {sorting_variables['sorting_date'].min():%Y-%m} to {sorting_variables['sorting_date'].max():%Y-%m}")
Size observations: 13,756
Market equity observations: 14,286
Book-to-market observations: 13,389
Final sorting variables: 12,046 stock-years
Sorting date range: 2011-07 to 2023-07
17.3.4 Kiểm chứng các biến phân loại
Trước khi tiếp tục, chúng ta kiểm chứng rằng các biến phân loại có phân phối hợp lý. Tỷ số giá trị sổ sách trên giá trị thị trường nên tập trung quanh 1,0 đối với một thị trường điển hình, mặc dù các thị trường mới nổi có thể khác.
Hiện mã
print("Sorting Variable Summary Statistics:")print(sorting_variables[["size", "bm", "op", "inv"]].describe().round(4))# Check for extreme values that might indicate data issuesprint(f"\nB/M Median: {sorting_variables['bm'].median():.4f}")print(f"B/M 1st percentile: {sorting_variables['bm'].quantile(0.01):.4f}")print(f"B/M 99th percentile: {sorting_variables['bm'].quantile(0.99):.4f}")
Các giá trị cực đoan trong đặc điểm dùng để phân loại có thể làm méo việc gán danh mục và lợi nhuận nhân tố. Chúng ta áp dụng winsorization (cắt đuôi giá trị cực đoan) để hạn chế ảnh hưởng của các giá trị ngoại lai trong khi vẫn giữ nguyên thứ hạng chung của các cổ phiếu.
Hiện mã
# Check BEFORE winsorizationprint("BEFORE Winsorization:")print(sorting_variables[["size", "bm", "op", "inv"]].describe().round(4))# Apply winsorizationdef winsorize_characteristics(df, columns, limits=(0.01, 0.99)):""" Apply winsorization using pandas clip. """ df = df.copy()for col in columns:if col in df.columns: lower = df[col].quantile(limits[0]) upper = df[col].quantile(limits[1]) df[col] = df[col].clip(lower=lower, upper=upper)print(f" {col}: clipped to [{lower:.4f}, {upper:.4f}]")return dfsorting_variables = winsorize_characteristics( sorting_variables, columns=["bm", "op", "inv"], # Don't winsorize size limits=(0.01, 0.99))# Check AFTER winsorizationprint("\nAFTER Winsorization:")print(sorting_variables[["size", "bm", "op", "inv"]].describe().round(4))
BEFORE Winsorization:
size bm op inv
count 12046.0000 12046.0000 12046.0000 12046.0000
mean 2225.5648 1.7033 0.1852 0.1322
std 14680.4225 3.8683 0.2782 2.5410
min 0.4864 0.0014 -0.7529 -0.9569
25% 62.7556 0.7595 0.0309 -0.0495
50% 182.6410 1.1849 0.1367 0.0342
75% 641.8896 1.8853 0.2952 0.1582
max 426020.9817 272.1893 1.4256 261.3355
bm: clipped to [0.1710, 8.0262]
op: clipped to [-0.7319, 1.2192]
inv: clipped to [-0.3990, 1.5195]
AFTER Winsorization:
size bm op inv
count 12046.0000 12046.0000 12046.0000 12046.0000
mean 2225.5648 1.5544 0.1837 0.0894
std 14680.4225 1.2843 0.2705 0.2721
min 0.4864 0.1710 -0.7319 -0.3990
25% 62.7556 0.7595 0.0309 -0.0495
50% 182.6410 1.1849 0.1367 0.0342
75% 641.8896 1.8853 0.2952 0.1582
max 426020.9817 8.0262 1.2192 1.5195
17.4 Các hàm gán danh mục
17.4.1 Hàm gán danh mục
Chúng ta tạo một hàm linh hoạt để gán cổ phiếu vào các danh mục dựa trên các điểm cắt phân vị. Hàm này xử lý cả phân loại độc lập (các điểm cắt được tính trên tất cả cổ phiếu) và phân loại phụ thuộc (các điểm cắt được tính trong từng nhóm con).
Hiện mã
def assign_portfolio(data, sorting_variable, percentiles):"""Assign portfolios to a bin according to a sorting variable."""# Get the values values = data[sorting_variable].dropna()iflen(values) ==0:return pd.Series([np.nan] *len(data), index=data.index)# Calculate breakpoints breakpoints = values.quantile(percentiles, interpolation="linear")# Handle duplicate breakpoints by using unique values unique_breakpoints = np.unique(breakpoints)# If all values are the same, assign all to portfolio 1iflen(unique_breakpoints) <=1:return pd.Series([1] *len(data), index=data.index)# Set boundaries to -inf and +inf unique_breakpoints.iloc[0] =-np.inf unique_breakpoints.iloc[unique_breakpoints.size-1] = np.inf# Assign to bins assigned = pd.cut( data[sorting_variable], bins=unique_breakpoints, labels=pd.Series(range(1, breakpoints.size)), include_lowest=True, right=False )return assigned
Hiện mã
# Check the distribution of characteristics BEFORE portfolio assignmentprint("Operating Profitability Distribution:")print(sorting_variables["op"].describe())print(f"\nUnique OP values: {sorting_variables['op'].nunique()}")print("\nInvestment Distribution:")print(sorting_variables["inv"].describe())print(f"\nUnique INV values: {sorting_variables['inv'].nunique()}")# Check breakpoints for a specific datetest_date = sorting_variables["sorting_date"].iloc[0]test_data = sorting_variables.query("sorting_date == @test_date")print(f"\nBreakpoints for {test_date}:")print(f"OP 30th percentile: {test_data['op'].quantile(0.3):.4f}")print(f"OP 70th percentile: {test_data['op'].quantile(0.7):.4f}")print(f"INV 30th percentile: {test_data['inv'].quantile(0.3):.4f}")print(f"INV 70th percentile: {test_data['inv'].quantile(0.7):.4f}")
Operating Profitability Distribution:
count 12046.000000
mean 0.183738
std 0.270509
min -0.731888
25% 0.030913
50% 0.136675
75% 0.295185
max 1.219223
Name: op, dtype: float64
Unique OP values: 11804
Investment Distribution:
count 12046.000000
mean 0.089388
std 0.272147
min -0.399042
25% -0.049497
50% 0.034157
75% 0.158155
max 1.519474
Name: inv, dtype: float64
Unique INV values: 11805
Breakpoints for 2019-07-01 00:00:00:
OP 30th percentile: 0.0541
OP 70th percentile: 0.2566
INV 30th percentile: -0.0343
INV 70th percentile: 0.1116
17.4.2 Gán danh mục cho mô hình ba nhân tố
Đối với mô hình ba nhân tố, chúng ta thực hiện phân loại kép độc lập theo quy mô và tỷ số giá trị sổ sách trên giá trị thị trường. Quy mô được chia tại trung vị (2 nhóm), còn tỷ số giá trị sổ sách trên giá trị thị trường được chia tại phân vị thứ 30 và 70 (3 nhóm), tạo ra 6 danh mục.
Chúng ta hợp nhất việc gán danh mục với lợi nhuận tháng. Điểm mấu chốt là các danh mục hình thành vào tháng Bảy của năm \(t\) được nắm giữ đến hết tháng Sáu của năm \(t+1\). Chúng ta triển khai điều này bằng cách tính một biến sorting_date cho mỗi quan sát lợi nhuận tháng.
Hiện mã
def merge_portfolios_with_returns(prices_monthly, portfolio_assignments):""" Merge portfolio assignments with monthly returns. Portfolios formed in July t are held through June t+1. Parameters ---------- prices_monthly : pd.DataFrame Monthly stock returns portfolio_assignments : pd.DataFrame Portfolio assignments with sorting_date Returns ------- pd.DataFrame Returns merged with portfolio assignments """ portfolios = (prices_monthly .assign(# Map each return month to its portfolio formation date sorting_date=lambda x: pd.to_datetime( np.where( x["date"].dt.month <=6, (x["date"].dt.year -1).astype(str) +"-07-01", x["date"].dt.year.astype(str) +"-07-01" ) ) ) .merge( portfolio_assignments, on=["symbol", "sorting_date"], how="inner" ) )return portfoliosportfolios_monthly_ff3 = merge_portfolios_with_returns( prices_monthly, portfolios_ff3[["symbol", "sorting_date", "portfolio_size", "portfolio_bm"]])print(f"Merged observations: {len(portfolios_monthly_ff3):,}")
Merged observations: 136,444
17.5.2 Tính lợi nhuận danh mục theo trọng số vốn hóa
Chúng ta tính lợi nhuận theo trọng số vốn hóa cho từng danh mục trong sáu danh mục. Trọng số vốn hóa dùng vốn hóa thị trường trễ một kỳ để tránh thiên lệch nhìn trước.
Hiện mã
def compute_portfolio_returns(data, grouping_vars):""" Compute value-weighted portfolio returns. Parameters ---------- data : pd.DataFrame Returns data with portfolio assignments and mktcap_lag grouping_vars : list Variables defining portfolio groups Returns ------- pd.DataFrame Value-weighted returns for each portfolio-date """ portfolio_returns = (data .groupby(grouping_vars + ["date"], observed=True) .apply(lambda x: pd.Series({"ret": np.average(x["ret_excess"], weights=x["mktcap_lag"]),"n_stocks": len(x) })) .reset_index() )return portfolio_returns# Compute portfolio returnsportfolio_returns_ff3 = compute_portfolio_returns( portfolios_monthly_ff3, ["portfolio_size", "portfolio_bm"])print("Portfolio Returns Summary:")print(portfolio_returns_ff3.groupby(["portfolio_size", "portfolio_bm"], observed=True)["ret"].describe().round(4))
Nhân tố thị trường là lợi nhuận theo trọng số vốn hóa của tất cả cổ phiếu trừ lãi suất phi rủi ro. Chúng ta tính nhân tố này độc lập với các danh mục đã phân loại.
factors_ff3_monthly.to_sql( name="factors_ff3_monthly", con=tidy_finance, if_exists="replace", index=False)print("Three-factor monthly data saved to database.")
Three-factor monthly data saved to database.
17.6 Mô hình năm nhân tố Fama-French (tháng)
17.6.1 Gán danh mục với phân loại phụ thuộc
Đối với mô hình năm nhân tố, chúng ta dùng phân loại phụ thuộc: quy mô được phân loại độc lập, nhưng khả năng sinh lời và đầu tư được phân loại trong từng nhóm quy mô. Cách này kiểm soát tương quan giữa quy mô và các đặc điểm đó.
# Check characteristic monotonicity for each dimensionprint("Profitability by Portfolio (should be increasing):")print(portfolios_ff5.groupby("portfolio_op", observed=True)["op"].median().round(4))print("\nInvestment by Portfolio (should be increasing):")print(portfolios_ff5.groupby("portfolio_inv", observed=True)["inv"].median().round(4))# Check portfolio countsprint("\nStocks per Size/Profitability Bin:")print(portfolios_ff5.groupby(["portfolio_size", "portfolio_op"], observed=True).size().unstack(fill_value=0))# Check number of unique firms per year(portfolios_ff5 .groupby("sorting_date")["symbol"] .nunique())
factors_ff5_monthly.to_sql( name="factors_ff5_monthly", con=tidy_finance, if_exists="replace", index=False)print("Five-factor monthly data saved to database.")
Five-factor monthly data saved to database.
17.7 Các nhân tố Fama-French ngày
17.7.1 Động cơ của các nhân tố ngày
Các nhân tố ngày là thiết yếu cho một số ứng dụng:
Ước lượng beta ngày: Các hồi quy CAPM dùng dữ liệu ngày đòi hỏi lợi nhuận vượt mức thị trường theo ngày.
Nghiên cứu sự kiện: Đo lợi nhuận bất thường quanh các sự kiện doanh nghiệp đòi hỏi điều chỉnh nhân tố theo ngày.
Nghiên cứu tần suất cao: Các nghiên cứu vi cấu trúc thị trường cần dữ liệu nhân tố theo ngày hoặc trong ngày.
Phương pháp xây dựng tương tự cách tiếp cận theo tháng, nhưng chúng ta tính lợi nhuận danh mục ở tần suất ngày trong khi vẫn giữ nguyên các ngày hình thành danh mục hằng năm.
Daily returns: 3,462,157 observations
Date range: 2010-01-05 to 2023-12-29
17.7.3 Bổ sung vốn hóa để tính trọng số theo ngày
Để tính lợi nhuận ngày theo trọng số vốn hóa, chúng ta cần vốn hóa thị trường. Chúng ta dùng vốn hóa thị trường tháng gần nhất làm trọng số cho lợi nhuận ngày trong tháng đó.
Hiện mã
# Get monthly market cap to use as weights for daily returnsmktcap_monthly = (prices_monthly [["symbol", "date", "mktcap_lag"]] .assign(year_month=lambda x: x["date"].dt.to_period("M")))# Add year_month to daily data for mergingprices_daily = (prices_daily .assign(year_month=lambda x: x["date"].dt.to_period("M")) .merge( mktcap_monthly[["symbol", "year_month", "mktcap_lag"]], on=["symbol", "year_month"], how="left" ) .dropna(subset=["ret_excess", "mktcap_lag"]))print(f"Daily returns with weights: {len(prices_daily):,} observations")
Daily returns with weights: 3,443,815 observations
17.7.4 Hợp nhất lợi nhuận ngày với các danh mục
Chúng ta dùng cùng việc gán danh mục (hình thành hằng năm vào tháng Bảy) cho lợi nhuận ngày.
Hình 17.1: Lợi nhuận tích lũy của các nhân tố Fama-French cho thị trường Việt Nam. Hình thể hiện mức tăng trưởng của $1 đầu tư vào từng danh mục nhân tố.
17.8.2 Phần bù nhân tố trung bình
Chúng ta tính phần bù nhân tố trung bình quy năm và ý nghĩa thống kê của chúng.
Giải thích các mô hình nhân tố: Mô hình ba nhân tố Fama-French bổ sung các nhân tố quy mô (SMB) và giá trị (HML) vào CAPM, trong khi mô hình năm nhân tố bao gồm thêm các nhân tố khả năng sinh lời (RMW) và đầu tư (CMA).
Phương pháp xây dựng: Các nhân tố được xây dựng thông qua các danh mục phân loại kép với sự chú ý cẩn thận đến thời điểm. Các danh mục được hình thành vào tháng Bảy, dùng dữ liệu kế toán của năm tài chính trước đó để bảo đảm thông tin đã được công bố công khai.
Phân loại độc lập và phụ thuộc: Mô hình ba nhân tố dùng phân loại độc lập theo quy mô và tỷ số giá trị sổ sách trên giá trị thị trường, tạo ra lưới 2×3. Mô hình năm nhân tố dùng phân loại phụ thuộc, trong đó các đặc điểm được phân loại trong từng nhóm quy mô.
Lợi nhuận theo trọng số vốn hóa: Lợi nhuận danh mục được tính theo trọng số vốn hóa với vốn hóa thị trường trễ một kỳ để tránh thiên lệch nhìn trước.
Nhân tố ngày: Các nhân tố ngày dùng cùng việc gán danh mục hằng năm nhưng tính lợi nhuận ở tần suất ngày, cho phép các ứng dụng tần suất cao hơn như ước lượng beta ngày.
Nhân tố thị trường: Nhân tố thị trường được tính độc lập bằng lợi nhuận theo trọng số vốn hóa của tất cả cổ phiếu trừ lãi suất phi rủi ro.
Kiểm chứng: Chất lượng nhân tố có thể được đánh giá thông qua tính đơn điệu của đặc điểm, mức đa dạng hóa của danh mục, lợi nhuận tích lũy và tính nhất quán giữa tần suất ngày và tháng.
Điều chỉnh cho thị trường Việt Nam: Trong khi bám theo phương pháp Fama-French gốc, chúng ta điều chỉnh cho phù hợp với đặc điểm thị trường Việt Nam, bao gồm chuẩn mực kế toán VAS, thời hạn báo cáo và đơn vị tiền tệ.
Fama, Eugene F., và Kenneth R. French. 1992. “The cross-section of expected stock returns”. The Journal of Finance 47 (2): 427–65. https://doi.org/2329112.
# Các nhân tố Fama-FrenchChương này trình bày việc tái lập các danh mục nhân tố Fama-French cho thị trường chứng khoán Việt Nam. Các mô hình nhân tố Fama-French là nền tảng của định giá tài sản thực nghiệm, bắt nguồn từ công trình kinh điển của @Fama1992 và sau đó được mở rộng trong @FamaFrench2015. Các mô hình này đã thay đổi cách giới học thuật và nhà thực hành hiểu về sự khác biệt theo chiều ngang của lợi nhuận kỳ vọng cổ phiếu, vượt ra ngoài mô hình định giá tài sản vốn một nhân tố để đưa vào nhiều nguồn rủi ro hệ thống.Chúng ta xây dựng cả mô hình ba nhân tố và năm nhân tố ở tần suất tháng và ngày. Các nhân tố tháng là nền tảng cho hầu hết các kiểm định định giá tài sản và phân tích danh mục, trong khi các nhân tố ngày cho phép các ứng dụng tần suất cao hơn, bao gồm nghiên cứu sự kiện ngắn hạn, nghiên cứu vi cấu trúc thị trường và ước lượng beta ngày. Bằng cách xây dựng nhân tố ở cả hai tần suất, chúng ta tạo ra một bộ công cụ hoàn chỉnh cho nghiên cứu tài chính thực nghiệm trên thị trường Việt Nam.Chương được triển khai như sau. Trước hết, chúng ta bàn về động cơ lý thuyết của từng nhân tố và trực giác kinh tế đằng sau phương pháp Fama-French. Sau đó, chúng ta chuẩn bị dữ liệu cần thiết, hợp nhất lợi nhuận cổ phiếu với các đặc điểm kế toán. Tiếp theo, chúng ta triển khai các quy trình phân loại danh mục làm cơ sở cho việc xây dựng nhân tố, bám sát các quy trình Fama-French gốc đồng thời điều chỉnh cho phù hợp với đặc điểm thị trường Việt Nam. Chúng ta xây dựng mô hình ba nhân tố (thị trường, quy mô và giá trị) trước khi mở rộng sang mô hình năm nhân tố (bổ sung khả năng sinh lời và đầu tư). Cuối cùng, chúng ta xây dựng các nhân tố ngày và kiểm chứng các nhân tố đã tái lập thông qua nhiều kiểm tra chẩn đoán.## Cơ sở lý thuyết### Quá trình phát triển từ CAPM đến các mô hình đa nhân tốMô hình định giá tài sản vốn của @Sharpe1964 cho rằng một nhân tố duy nhất, danh mục thị trường, phải giải thích toàn bộ sự khác biệt theo chiều ngang của lợi nhuận kỳ vọng. Tuy nhiên, nhiều thập kỷ nghiên cứu thực nghiệm đã ghi nhận những quy luật dai dẳng mà CAPM không giải thích được. @Fama1992 đã chứng minh rằng hai đặc điểm của doanh nghiệp, quy mô và tỷ số giá trị sổ sách trên giá trị thị trường, nắm bắt một phần đáng kể biến thiên của lợi nhuận trung bình mà beta thị trường không giải thích được.Các doanh nghiệp nhỏ có xu hướng kiếm được lợi nhuận cao hơn các doanh nghiệp lớn, một quy luật được gọi là hiệu ứng quy mô. Tương tự, các doanh nghiệp có tỷ số giá trị sổ sách trên giá trị thị trường cao (cổ phiếu giá trị) có xu hướng vượt trội các doanh nghiệp có tỷ số này thấp (cổ phiếu tăng trưởng), được gọi là phần bù giá trị. Mô hình ba nhân tố chính thức hóa các quan sát này bằng cách xây dựng các danh mục nhân tố có thể giao dịch được:$$r_{i,t} - r_{f,t} = \alpha_i + \beta_i^{MKT}(r_{m,t} - r_{f,t}) + \beta_i^{SMB} \cdot SMB_t + \beta_i^{HML} \cdot HML_t + \varepsilon_{i,t}$$ {#eq-ff3}trong đó:- $r_{i,t} - r_{f,t}$ là lợi nhuận vượt mức của tài sản $i$- $r_{m,t} - r_{f,t}$ là lợi nhuận vượt mức của thị trường- $SMB_t$ (Small Minus Big) là nhân tố quy mô- $HML_t$ (High Minus Low) là nhân tố giá trị### Mở rộng sang năm nhân tố@FamaFrench2015 mở rộng mô hình để bao gồm hai nhân tố bổ sung, được gợi ý từ mô hình chiết khấu cổ tức. Các doanh nghiệp có khả năng sinh lời cao hơn phải có lợi nhuận kỳ vọng cao hơn (các yếu tố khác không đổi), và các doanh nghiệp có chính sách đầu tư mạnh tay phải có lợi nhuận kỳ vọng thấp hơn:$$r_{i,t} - r_{f,t} = \alpha_i + \beta_i^{MKT}MKT_t + \beta_i^{SMB}SMB_t + \beta_i^{HML}HML_t + \beta_i^{RMW}RMW_t + \beta_i^{CMA}CMA_t + \varepsilon_{i,t}$$ {#eq-ff5}trong đó:- $RMW_t$ (Robust Minus Weak) là nhân tố khả năng sinh lời- $CMA_t$ (Conservative Minus Aggressive) là nhân tố đầu tư### Phương pháp xây dựng nhân tốPhương pháp Fama-French xây dựng các nhân tố thông qua các danh mục phân loại kép:1. **Phân loại theo quy mô**: Cổ phiếu được chia thành nhóm Nhỏ và Lớn dựa trên vốn hóa thị trường trung vị.2. **Phân loại theo đặc điểm**: Trong mỗi nhóm quy mô, cổ phiếu được phân loại thành ba nhóm theo tam phân vị dựa trên tỷ số giá trị sổ sách trên giá trị thị trường (cho HML), khả năng sinh lời hoạt động (cho RMW) hoặc đầu tư (cho CMA).3. **Lợi nhuận nhân tố**: Các nhân tố được tính bằng chênh lệch giữa lợi nhuận trung bình của các danh mục có giá trị đặc điểm cao so với thấp, lấy trung bình qua các nhóm quy mô để loại bỏ hiệu ứng quy mô.4. **Thời điểm**: Các danh mục được hình thành vào cuối tháng Sáu mỗi năm, dùng dữ liệu kế toán của năm tài chính trước đó, bảo đảm toàn bộ thông tin đã được công bố công khai tại thời điểm hình thành.## Thiết lập môi trườngChúng ta nạp các gói Python cần thiết để xử lý dữ liệu, phân tích thống kê và trực quan hóa.```{python}import pandas as pdimport numpy as npimport sqlite3import statsmodels.formula.api as smffrom scipy.stats.mstats import winsorizeimport matplotlib.pyplot as pltfrom plotnine import*from mizani.formatters import percent_format, comma_format```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ý.```{python}tidy_finance = sqlite3.connect(database="data/tidy_finance_python.sqlite")```## Chuẩn bị dữ liệu### Nạp lợi nhuận cổ phiếuChúng ta nạp dữ liệu lợi nhuận cổ phiếu theo tháng, bao gồm lợi nhuận vượt mức, vốn hóa thị trường và lãi suất phi rủi ro. Các biến này là thiết yếu để tính lợi nhuận danh mục theo trọng số vốn hóa và phần bù nhân tố.```{python}prices_monthly = pd.read_sql_query( sql=""" SELECT symbol, date, ret_excess, mktcap, mktcap_lag, risk_free FROM prices_monthly """, con=tidy_finance, parse_dates={"date"}).dropna()print(f"Monthly returns: {len(prices_monthly):,} observations")print(f"Unique stocks: {prices_monthly['symbol'].nunique():,}")print(f"Date range: {prices_monthly['date'].min():%Y-%m} to {prices_monthly['date'].max():%Y-%m}")```### Nạp dữ liệu cơ bản của doanh nghiệpChúng ta nạp dữ liệu cơ bản của doanh nghiệp gồm vốn chủ sở hữu sổ sách, khả năng sinh lời hoạt động và đầu tư, tức là các đặc điểm cần thiết để xây dựng các nhân tố Fama-French.```{python}comp_vn = pd.read_sql_query( sql=""" SELECT symbol, datadate, be, op, inv FROM comp_vn """, con=tidy_finance, parse_dates={"datadate"}).dropna()print(f"Fundamentals: {len(comp_vn):,} firm-year observations")print(f"Unique firms: {comp_vn['symbol'].nunique():,}")```### Xây dựng các biến phân loạiTheo quy ước Fama-French, chúng ta xây dựng các biến phân loại với sự chú ý cẩn thận đến thời điểm. Các nguyên tắc chính là:1. **Quy mô (vốn hóa tháng Sáu)**: Chúng ta dùng vốn hóa thị trường vào cuối tháng Sáu của năm $t$ để phân loại cổ phiếu vào các nhóm quy mô. Điều này bảo đảm chúng ta nắm bắt quy mô của doanh nghiệp vào đúng thời điểm hình thành danh mục.2. **Tỷ số giá trị sổ sách trên giá trị thị trường**: Chúng ta dùng vốn chủ sở hữu sổ sách của năm tài chính $t-1$ chia cho vốn hóa thị trường vào cuối tháng Mười Hai $t-1$. Điều này tạo ra độ trễ sáu tháng giữa dữ liệu kế toán và thời điểm hình thành danh mục, bảo đảm thông tin đã được công bố công khai.3. **Ngày hình thành danh mục**: Các danh mục được hình thành vào ngày 1 tháng Bảy và được nắm giữ mười hai tháng cho đến tháng Sáu năm sau.```{python}def construct_sorting_variables(prices_monthly, comp_vn):""" Construct sorting variables following Fama-French methodology. Parameters ---------- prices_monthly : pd.DataFrame Monthly stock returns with market cap comp_vn : pd.DataFrame Company fundamentals with book equity, profitability, investment Returns ------- pd.DataFrame Sorting variables aligned with July 1st formation dates """# 1. Size: June market capitalization# Portfolio formation is July 1st, so we use June market cap size = (prices_monthly .query("date.dt.month == 6") .assign( sorting_date=lambda x: x["date"] + pd.offsets.MonthBegin(1) ) [["symbol", "sorting_date", "mktcap"]] .rename(columns={"mktcap": "size"}) )print(f"Size observations: {len(size):,}")# 2. Market Equity: December market cap for B/M calculation# December t-1 market cap is used with fiscal year t-1 book equity# This is then used for July t portfolio formation market_equity = (prices_monthly .query("date.dt.month == 12") .assign(# December year t-1 maps to July year t formation sorting_date=lambda x: x["date"] + pd.offsets.MonthBegin(7) ) [["symbol", "sorting_date", "mktcap"]] .rename(columns={"mktcap": "me"}) )print(f"Market equity observations: {len(market_equity):,}")# 3. Book-to-Market and other characteristics# Fiscal year t-1 data is used for July t portfolio formation book_to_market = (comp_vn .assign(# Fiscal year-end + 6 months = July formation sorting_date=lambda x: pd.to_datetime( (x["datadate"].dt.year +1).astype(str) +"-07-01" ) ) .merge(market_equity, on=["symbol", "sorting_date"], how="inner") .assign(# Scale book equity to match market equity units# BE is in VND, ME is in millions VND bm=lambda x: x["be"] / (x["me"] *1e9) ) [["symbol", "sorting_date", "me", "bm", "op", "inv"]] )print(f"Book-to-market observations: {len(book_to_market):,}")# 4. Merge size with characteristics sorting_variables = (size .merge(book_to_market, on=["symbol", "sorting_date"], how="inner") .dropna() .drop_duplicates(subset=["symbol", "sorting_date"]) )return sorting_variablessorting_variables = construct_sorting_variables(prices_monthly, comp_vn)print(f"\nFinal sorting variables: {len(sorting_variables):,} stock-years")print(f"Sorting date range: {sorting_variables['sorting_date'].min():%Y-%m} to {sorting_variables['sorting_date'].max():%Y-%m}")```### Kiểm chứng các biến phân loạiTrước khi tiếp tục, chúng ta kiểm chứng rằng các biến phân loại có phân phối hợp lý. Tỷ số giá trị sổ sách trên giá trị thị trường nên tập trung quanh 1,0 đối với một thị trường điển hình, mặc dù các thị trường mới nổi có thể khác.```{python}print("Sorting Variable Summary Statistics:")print(sorting_variables[["size", "bm", "op", "inv"]].describe().round(4))# Check for extreme values that might indicate data issuesprint(f"\nB/M Median: {sorting_variables['bm'].median():.4f}")print(f"B/M 1st percentile: {sorting_variables['bm'].quantile(0.01):.4f}")print(f"B/M 99th percentile: {sorting_variables['bm'].quantile(0.99):.4f}")```### Xử lý các giá trị ngoại laiCác giá trị cực đoan trong đặc điểm dùng để phân loại có thể làm méo việc gán danh mục và lợi nhuận nhân tố. Chúng ta áp dụng winsorization (cắt đuôi giá trị cực đoan) để hạn chế ảnh hưởng của các giá trị ngoại lai trong khi vẫn giữ nguyên thứ hạng chung của các cổ phiếu.```{python}# Check BEFORE winsorizationprint("BEFORE Winsorization:")print(sorting_variables[["size", "bm", "op", "inv"]].describe().round(4))# Apply winsorizationdef winsorize_characteristics(df, columns, limits=(0.01, 0.99)):""" Apply winsorization using pandas clip. """ df = df.copy()for col in columns:if col in df.columns: lower = df[col].quantile(limits[0]) upper = df[col].quantile(limits[1]) df[col] = df[col].clip(lower=lower, upper=upper)print(f" {col}: clipped to [{lower:.4f}, {upper:.4f}]")return dfsorting_variables = winsorize_characteristics( sorting_variables, columns=["bm", "op", "inv"], # Don't winsorize size limits=(0.01, 0.99))# Check AFTER winsorizationprint("\nAFTER Winsorization:")print(sorting_variables[["size", "bm", "op", "inv"]].describe().round(4))```## Các hàm gán danh mục### Hàm gán danh mụcChúng ta tạo một hàm linh hoạt để gán cổ phiếu vào các danh mục dựa trên các điểm cắt phân vị. Hàm này xử lý cả phân loại độc lập (các điểm cắt được tính trên tất cả cổ phiếu) và phân loại phụ thuộc (các điểm cắt được tính trong từng nhóm con).```{python}def assign_portfolio(data, sorting_variable, percentiles):"""Assign portfolios to a bin according to a sorting variable."""# Get the values values = data[sorting_variable].dropna()iflen(values) ==0:return pd.Series([np.nan] *len(data), index=data.index)# Calculate breakpoints breakpoints = values.quantile(percentiles, interpolation="linear")# Handle duplicate breakpoints by using unique values unique_breakpoints = np.unique(breakpoints)# If all values are the same, assign all to portfolio 1iflen(unique_breakpoints) <=1:return pd.Series([1] *len(data), index=data.index)# Set boundaries to -inf and +inf unique_breakpoints.iloc[0] =-np.inf unique_breakpoints.iloc[unique_breakpoints.size-1] = np.inf# Assign to bins assigned = pd.cut( data[sorting_variable], bins=unique_breakpoints, labels=pd.Series(range(1, breakpoints.size)), include_lowest=True, right=False )return assigned``````{python}# Check the distribution of characteristics BEFORE portfolio assignmentprint("Operating Profitability Distribution:")print(sorting_variables["op"].describe())print(f"\nUnique OP values: {sorting_variables['op'].nunique()}")print("\nInvestment Distribution:")print(sorting_variables["inv"].describe())print(f"\nUnique INV values: {sorting_variables['inv'].nunique()}")# Check breakpoints for a specific datetest_date = sorting_variables["sorting_date"].iloc[0]test_data = sorting_variables.query("sorting_date == @test_date")print(f"\nBreakpoints for {test_date}:")print(f"OP 30th percentile: {test_data['op'].quantile(0.3):.4f}")print(f"OP 70th percentile: {test_data['op'].quantile(0.7):.4f}")print(f"INV 30th percentile: {test_data['inv'].quantile(0.3):.4f}")print(f"INV 70th percentile: {test_data['inv'].quantile(0.7):.4f}")```### Gán danh mục cho mô hình ba nhân tốĐối với mô hình ba nhân tố, chúng ta thực hiện phân loại kép độc lập theo quy mô và tỷ số giá trị sổ sách trên giá trị thị trường. Quy mô được chia tại trung vị (2 nhóm), còn tỷ số giá trị sổ sách trên giá trị thị trường được chia tại phân vị thứ 30 và 70 (3 nhóm), tạo ra 6 danh mục.```{python}def assign_ff3_portfolios(sorting_variables):""" Assign portfolios for Fama-French three-factor model. Independent 2x3 sort on size and book-to-market. """ df = sorting_variables.copy()# Independent size sort (median split) df["portfolio_size"] = df.groupby("sorting_date")["size"].transform(lambda x: pd.qcut(x, q=[0, 0.5, 1], labels=[1, 2], duplicates='drop') )# Independent B/M sort (30/70 split) df["portfolio_bm"] = df.groupby("sorting_date")["bm"].transform(lambda x: pd.qcut(x, q=[0, 0.3, 0.7, 1], labels=[1, 2, 3], duplicates='drop') )return df# Assign portfoliosportfolios_ff3 = assign_ff3_portfolios(sorting_variables)# Validateprint("FF3 Book-to-Market by Portfolio (should be INCREASING):")print(portfolios_ff3.groupby("portfolio_bm", observed=True)["bm"].median().round(4))print("Three-Factor Portfolio Assignments:")print(portfolios_ff3[["symbol", "sorting_date", "portfolio_size", "portfolio_bm"]].head(10))```### Kiểm chứng việc gán danh mụcChúng ta kiểm tra rằng việc gán danh mục tạo ra lưới 2×3 như kỳ vọng với số lượng cổ phiếu hợp lý trong mỗi ô.```{python}# Check portfolio distribution for most recent yearlatest_date = portfolios_ff3["sorting_date"].max()portfolio_counts = (portfolios_ff3 .query("sorting_date == @latest_date") .groupby(["portfolio_size", "portfolio_bm"], observed=True) .size() .unstack(fill_value=0))print(f"Portfolio Counts for {latest_date:%Y-%m}:")print(portfolio_counts)# Verify characteristic monotonicityprint("\nBook-to-Market by Portfolio (should be increasing):")print(portfolios_ff3.groupby("portfolio_bm", observed=True)["bm"].median().round(4))```Chúng ta kiểm tra rằng, đối với một cổ phiếu, việc gán danh mục không đổi từ tháng Bảy của năm này đến tháng Sáu của năm sau.```{python}# Trace a single symbol (e.g., 'A32') across a formation windowpersistence_check = (portfolios_ff3 .query("symbol == 'A32' & sorting_date >= '2022-01-01' & sorting_date <= '2023-12-31'") .sort_values("sorting_date") [['symbol', 'sorting_date', 'portfolio_size', 'portfolio_bm']])print("\nTemporal Persistence Check (Symbol A32):")print(persistence_check.head(15))```## Mô hình ba nhân tố Fama-French (tháng)### Hợp nhất danh mục với lợi nhuậnChúng ta hợp nhất việc gán danh mục với lợi nhuận tháng. Điểm mấu chốt là các danh mục hình thành vào tháng Bảy của năm $t$ được nắm giữ đến hết tháng Sáu của năm $t+1$. Chúng ta triển khai điều này bằng cách tính một biến `sorting_date` cho mỗi quan sát lợi nhuận tháng.```{python}def merge_portfolios_with_returns(prices_monthly, portfolio_assignments):""" Merge portfolio assignments with monthly returns. Portfolios formed in July t are held through June t+1. Parameters ---------- prices_monthly : pd.DataFrame Monthly stock returns portfolio_assignments : pd.DataFrame Portfolio assignments with sorting_date Returns ------- pd.DataFrame Returns merged with portfolio assignments """ portfolios = (prices_monthly .assign(# Map each return month to its portfolio formation date sorting_date=lambda x: pd.to_datetime( np.where( x["date"].dt.month <=6, (x["date"].dt.year -1).astype(str) +"-07-01", x["date"].dt.year.astype(str) +"-07-01" ) ) ) .merge( portfolio_assignments, on=["symbol", "sorting_date"], how="inner" ) )return portfoliosportfolios_monthly_ff3 = merge_portfolios_with_returns( prices_monthly, portfolios_ff3[["symbol", "sorting_date", "portfolio_size", "portfolio_bm"]])print(f"Merged observations: {len(portfolios_monthly_ff3):,}")```### Tính lợi nhuận danh mục theo trọng số vốn hóaChúng ta tính lợi nhuận theo trọng số vốn hóa cho từng danh mục trong sáu danh mục. Trọng số vốn hóa dùng vốn hóa thị trường trễ một kỳ để tránh thiên lệch nhìn trước.```{python}def compute_portfolio_returns(data, grouping_vars):""" Compute value-weighted portfolio returns. Parameters ---------- data : pd.DataFrame Returns data with portfolio assignments and mktcap_lag grouping_vars : list Variables defining portfolio groups Returns ------- pd.DataFrame Value-weighted returns for each portfolio-date """ portfolio_returns = (data .groupby(grouping_vars + ["date"], observed=True) .apply(lambda x: pd.Series({"ret": np.average(x["ret_excess"], weights=x["mktcap_lag"]),"n_stocks": len(x) })) .reset_index() )return portfolio_returns# Compute portfolio returnsportfolio_returns_ff3 = compute_portfolio_returns( portfolios_monthly_ff3, ["portfolio_size", "portfolio_bm"])print("Portfolio Returns Summary:")print(portfolio_returns_ff3.groupby(["portfolio_size", "portfolio_bm"], observed=True)["ret"].describe().round(4))```### Xây dựng các nhân tố SMB và HMLBây giờ chúng ta xây dựng các nhân tố SMB và HML từ lợi nhuận các danh mục.**SMB (Small Minus Big)**: Lợi nhuận trung bình của ba danh mục nhỏ trừ lợi nhuận trung bình của ba danh mục lớn.**HML (High Minus Low)**: Lợi nhuận trung bình của hai danh mục B/M cao trừ lợi nhuận trung bình của hai danh mục B/M thấp.```{python}def construct_ff3_factors(portfolio_returns):""" Construct Fama-French three factors from portfolio returns. Parameters ---------- portfolio_returns : pd.DataFrame Value-weighted returns for 2x3 portfolios Returns ------- pd.DataFrame Monthly SMB and HML factors """ factors = (portfolio_returns .groupby("date") .apply(lambda x: pd.Series({# SMB: Small minus Big (average across B/M groups)"smb": ( x.loc[x["portfolio_size"] ==1, "ret"].mean() - x.loc[x["portfolio_size"] ==2, "ret"].mean() ),# HML: High minus Low B/M (average across size groups)"hml": ( x.loc[x["portfolio_bm"] ==3, "ret"].mean() - x.loc[x["portfolio_bm"] ==1, "ret"].mean() ) })) .reset_index() )return factorsfactors_smb_hml = construct_ff3_factors(portfolio_returns_ff3)print("SMB and HML Factors:")print(factors_smb_hml.head(10))```### Tính nhân tố thị trườngNhân tố thị trường là lợi nhuận theo trọng số vốn hóa của tất cả cổ phiếu trừ lãi suất phi rủi ro. Chúng ta tính nhân tố này độc lập với các danh mục đã phân loại.```{python}def compute_market_factor(prices_monthly):""" Compute value-weighted market excess return. Parameters ---------- prices_monthly : pd.DataFrame Monthly stock returns with mktcap_lag Returns ------- pd.DataFrame Monthly market excess return """ market_factor = (prices_monthly .groupby("date") .apply(lambda x: pd.Series({"mkt_excess": np.average(x["ret_excess"], weights=x["mktcap_lag"]),"n_stocks": len(x) }), include_groups=False) .reset_index() )return market_factormarket_factor = compute_market_factor(prices_monthly)print("Market Factor Summary:")print(market_factor["mkt_excess"].describe().round(4))```### Kết hợp ba nhân tốChúng ta kết hợp SMB, HML và nhân tố thị trường thành bộ dữ liệu ba nhân tố hoàn chỉnh.```{python}factors_ff3_monthly = (factors_smb_hml .merge(market_factor[["date", "mkt_excess"]], on="date", how="inner"))# Add risk-free rate for completenessrf_monthly = (prices_monthly .groupby("date")["risk_free"] .first() .reset_index())factors_ff3_monthly = factors_ff3_monthly.merge(rf_monthly, on="date", how="left")print("Fama-French Three Factors (Monthly):")print(factors_ff3_monthly.head(10))print("\nFactor Summary Statistics:")print(factors_ff3_monthly[["mkt_excess", "smb", "hml"]].describe().round(4))```### Lưu dữ liệu ba nhân tố```{python}factors_ff3_monthly.to_sql( name="factors_ff3_monthly", con=tidy_finance, if_exists="replace", index=False)print("Three-factor monthly data saved to database.")```## Mô hình năm nhân tố Fama-French (tháng)### Gán danh mục với phân loại phụ thuộcĐối với mô hình năm nhân tố, chúng ta dùng phân loại phụ thuộc: quy mô được phân loại độc lập, nhưng khả năng sinh lời và đầu tư được phân loại trong từng nhóm quy mô. Cách này kiểm soát tương quan giữa quy mô và các đặc điểm đó.```{python}def assign_ff5_portfolios(sorting_variables):""" Assign portfolios for Fama-French five-factor model. """ df = sorting_variables.copy()# Independent size sort df["portfolio_size"] = df.groupby("sorting_date")["size"].transform(lambda x: pd.qcut(x, q=[0, 0.5, 1], labels=[1, 2], duplicates='drop') )# Dependent sorts within size groups df["portfolio_bm"] = df.groupby(["sorting_date", "portfolio_size"])["bm"].transform(lambda x: pd.qcut(x, q=[0, 0.3, 0.7, 1], labels=[1, 2, 3], duplicates='drop') ) df["portfolio_op"] = df.groupby(["sorting_date", "portfolio_size"])["op"].transform(lambda x: pd.qcut(x, q=[0, 0.3, 0.7, 1], labels=[1, 2, 3], duplicates='drop') ) df["portfolio_inv"] = df.groupby(["sorting_date", "portfolio_size"])["inv"].transform(lambda x: pd.qcut(x, q=[0, 0.3, 0.7, 1], labels=[1, 2, 3], duplicates='drop') )return df# Runportfolios_ff5 = assign_ff5_portfolios(sorting_variables)```### Kiểm chứng các danh mục năm nhân tố```{python}# Check characteristic monotonicity for each dimensionprint("Profitability by Portfolio (should be increasing):")print(portfolios_ff5.groupby("portfolio_op", observed=True)["op"].median().round(4))print("\nInvestment by Portfolio (should be increasing):")print(portfolios_ff5.groupby("portfolio_inv", observed=True)["inv"].median().round(4))# Check portfolio countsprint("\nStocks per Size/Profitability Bin:")print(portfolios_ff5.groupby(["portfolio_size", "portfolio_op"], observed=True).size().unstack(fill_value=0))# Check number of unique firms per year(portfolios_ff5 .groupby("sorting_date")["symbol"] .nunique())```### Hợp nhất và tính lợi nhuận danh mục```{python}# Merge with returnsportfolios_monthly_ff5 = merge_portfolios_with_returns( prices_monthly, portfolios_ff5[["symbol", "sorting_date", "portfolio_size", "portfolio_bm", "portfolio_op", "portfolio_inv"]])print(f"Five-factor merged observations: {len(portfolios_monthly_ff5):,}")```### Xây dựng cả năm nhân tốChúng ta xây dựng từng nhân tố từ các phép phân loại danh mục tương ứng.```{python}def construct_ff5_factors(portfolios_monthly):""" Construct Fama-French five factors from portfolio data. Parameters ---------- portfolios_monthly : pd.DataFrame Monthly returns with all portfolio assignments Returns ------- pd.DataFrame Monthly five-factor returns """# HML: Value factor from B/M sorts portfolios_bm = (portfolios_monthly .groupby(["portfolio_size", "portfolio_bm", "date"], observed=True) .apply(lambda x: pd.Series({"ret": np.average(x["ret_excess"], weights=x["mktcap_lag"]) })) .reset_index() ) factors_hml = (portfolios_bm .groupby("date") .apply(lambda x: pd.Series({"hml": (x.loc[x["portfolio_bm"] ==3, "ret"].mean() - x.loc[x["portfolio_bm"] ==1, "ret"].mean()) })) .reset_index() )# RMW: Profitability factor from OP sorts portfolios_op = (portfolios_monthly .groupby(["portfolio_size", "portfolio_op", "date"], observed=True) .apply(lambda x: pd.Series({"ret": np.average(x["ret_excess"], weights=x["mktcap_lag"]) })) .reset_index() ) factors_rmw = (portfolios_op .groupby("date") .apply(lambda x: pd.Series({"rmw": (x.loc[x["portfolio_op"] ==3, "ret"].mean() - x.loc[x["portfolio_op"] ==1, "ret"].mean()) })) .reset_index() )# CMA: Investment factor from INV sorts# Note: CMA is Conservative minus Aggressive (low inv - high inv) portfolios_inv = (portfolios_monthly .groupby(["portfolio_size", "portfolio_inv", "date"], observed=True) .apply(lambda x: pd.Series({"ret": np.average(x["ret_excess"], weights=x["mktcap_lag"]) })) .reset_index() ) factors_cma = (portfolios_inv .groupby("date") .apply(lambda x: pd.Series({"cma": (x.loc[x["portfolio_inv"] ==1, "ret"].mean() - x.loc[x["portfolio_inv"] ==3, "ret"].mean()) })) .reset_index() )# SMB: Size factor (average across all characteristic portfolios) all_portfolios = pd.concat([portfolios_bm, portfolios_op, portfolios_inv]) factors_smb = (all_portfolios .groupby("date") .apply(lambda x: pd.Series({"smb": (x.loc[x["portfolio_size"] ==1, "ret"].mean() - x.loc[x["portfolio_size"] ==2, "ret"].mean()) })) .reset_index() )# Combine all factors factors = (factors_smb .merge(factors_hml, on="date", how="outer") .merge(factors_rmw, on="date", how="outer") .merge(factors_cma, on="date", how="outer") )return factorsfactors_ff5 = construct_ff5_factors(portfolios_monthly_ff5)# Add market factorfactors_ff5_monthly = (factors_ff5 .merge(market_factor[["date", "mkt_excess"]], on="date", how="inner") .merge(rf_monthly, on="date", how="left"))print("Fama-French Five Factors (Monthly):")print(factors_ff5_monthly.head(10))print("\nFactor Summary Statistics:")print(factors_ff5_monthly[["mkt_excess", "smb", "hml", "rmw", "cma"]].describe().round(4))```### Tương quan giữa các nhân tốChúng ta xem xét tương quan giữa các nhân tố; nhìn chung chúng nên thấp để các nhân tố nắm bắt những nguồn rủi ro riêng biệt.```{python}print("Factor Correlation Matrix:")correlation_matrix = factors_ff5_monthly[["mkt_excess", "smb", "hml", "rmw", "cma"]].corr()print(correlation_matrix.round(3))```### Lưu dữ liệu năm nhân tố```{python}factors_ff5_monthly.to_sql( name="factors_ff5_monthly", con=tidy_finance, if_exists="replace", index=False)print("Five-factor monthly data saved to database.")```## Các nhân tố Fama-French ngày### Động cơ của các nhân tố ngàyCác nhân tố ngày là thiết yếu cho một số ứng dụng:1. **Ước lượng beta ngày**: Các hồi quy CAPM dùng dữ liệu ngày đòi hỏi lợi nhuận vượt mức thị trường theo ngày.2. **Nghiên cứu sự kiện**: Đo lợi nhuận bất thường quanh các sự kiện doanh nghiệp đòi hỏi điều chỉnh nhân tố theo ngày.3. **Nghiên cứu tần suất cao**: Các nghiên cứu vi cấu trúc thị trường cần dữ liệu nhân tố theo ngày hoặc trong ngày.Phương pháp xây dựng tương tự cách tiếp cận theo tháng, nhưng chúng ta tính lợi nhuận danh mục ở tần suất ngày trong khi vẫn giữ nguyên các ngày hình thành danh mục hằng năm.### Nạp lợi nhuận ngày```{python}# Load daily price dataprices_daily = pd.read_sql_query( sql=""" SELECT symbol, date, ret_excess FROM prices_daily """, con=tidy_finance, parse_dates={"date"})print(f"Daily returns: {len(prices_daily):,} observations")print(f"Date range: {prices_daily['date'].min():%Y-%m-%d} to {prices_daily['date'].max():%Y-%m-%d}")```### Bổ sung vốn hóa để tính trọng số theo ngàyĐể tính lợi nhuận ngày theo trọng số vốn hóa, chúng ta cần vốn hóa thị trường. Chúng ta dùng vốn hóa thị trường tháng gần nhất làm trọng số cho lợi nhuận ngày trong tháng đó.```{python}# Get monthly market cap to use as weights for daily returnsmktcap_monthly = (prices_monthly [["symbol", "date", "mktcap_lag"]] .assign(year_month=lambda x: x["date"].dt.to_period("M")))# Add year_month to daily data for mergingprices_daily = (prices_daily .assign(year_month=lambda x: x["date"].dt.to_period("M")) .merge( mktcap_monthly[["symbol", "year_month", "mktcap_lag"]], on=["symbol", "year_month"], how="left" ) .dropna(subset=["ret_excess", "mktcap_lag"]))print(f"Daily returns with weights: {len(prices_daily):,} observations")```### Hợp nhất lợi nhuận ngày với các danh mụcChúng ta dùng cùng việc gán danh mục (hình thành hằng năm vào tháng Bảy) cho lợi nhuận ngày.```{python}# Step 1: Ensure portfolios_ff3 has correct formatportfolios_ff3_clean = portfolios_ff3[["symbol", "sorting_date", "portfolio_size", "portfolio_bm"]].copy()portfolios_ff3_clean["sorting_date"] = pd.to_datetime(portfolios_ff3_clean["sorting_date"])print("Portfolio sorting dates:")print(portfolios_ff3_clean["sorting_date"].unique()[:5])# Step 2: Create sorting_date for daily dataprices_daily_with_sort = prices_daily.copy()prices_daily_with_sort["sorting_date"] = prices_daily_with_sort["date"].apply(lambda x: pd.Timestamp(f"{x.year}-07-01") if x.month >6else pd.Timestamp(f"{x.year -1}-07-01"))print("\nDaily sorting dates:")print(prices_daily_with_sort["sorting_date"].unique()[:5])# Step 3: Mergeportfolios_daily_ff3 = prices_daily_with_sort.merge( portfolios_ff3_clean, on=["symbol", "sorting_date"], how="inner")print(f"\nMerged daily observations: {len(portfolios_daily_ff3):,}")print(f"Unique dates: {portfolios_daily_ff3['date'].nunique():,}")# Step 4: Verify portfolio distributionprint("\nPortfolio distribution in daily data:")print(portfolios_daily_ff3.groupby(["portfolio_size", "portfolio_bm"], observed=True).size().unstack(fill_value=0))``````{python}# Diagnostic: Check the daily portfolio mergeprint("="*50)print("DIAGNOSTIC: Daily Portfolio Merge")print("="*50)print(f"\nDaily prices rows: {len(prices_daily):,}")print(f"Daily FF3 portfolios rows: {len(portfolios_daily_ff3):,}")print(f"Match rate: {len(portfolios_daily_ff3)/len(prices_daily)*100:.1f}%")# Check portfolio distribution in daily dataprint("\nDaily portfolio distribution:")print(portfolios_daily_ff3.groupby(["portfolio_size", "portfolio_bm"], observed=True).size().unstack(fill_value=0))# Check a specific datetest_date = portfolios_daily_ff3["date"].iloc[1000]print(f"\nSample date: {test_date}")print(portfolios_daily_ff3.query("date == @test_date").groupby(["portfolio_size", "portfolio_bm"], observed=True).size().unstack(fill_value=0))```### Tính ba nhân tố ngày```{python}def compute_daily_ff3_factors(portfolios_daily):""" Compute daily Fama-French three factors. Parameters ---------- portfolios_daily : pd.DataFrame Daily returns with portfolio assignments Returns ------- pd.DataFrame Daily SMB and HML factors """# Compute daily portfolio returns portfolio_returns = (portfolios_daily .groupby(["portfolio_size", "portfolio_bm", "date"], observed=True) .apply(lambda x: pd.Series({"ret": np.average(x["ret_excess"], weights=x["mktcap_lag"]) })) .reset_index() )# Compute factors factors = (portfolio_returns .groupby("date") .apply(lambda x: pd.Series({"smb": (x.loc[x["portfolio_size"] ==1, "ret"].mean() - x.loc[x["portfolio_size"] ==2, "ret"].mean()),"hml": (x.loc[x["portfolio_bm"] ==3, "ret"].mean() - x.loc[x["portfolio_bm"] ==1, "ret"].mean()) })) .reset_index() )return factorsfactors_daily_smb_hml = compute_daily_ff3_factors(portfolios_daily_ff3)print(f"Daily factor observations: {len(factors_daily_smb_hml):,}")print(factors_daily_smb_hml.head(10))```### Tính nhân tố thị trường ngày```{python}def compute_daily_market_factor(prices_daily):""" Compute daily value-weighted market excess return. Parameters ---------- prices_daily : pd.DataFrame Daily returns with mktcap_lag Returns ------- pd.DataFrame Daily market excess return """ market_daily = (prices_daily .groupby("date") .apply(lambda x: pd.Series({"mkt_excess": np.average(x["ret_excess"], weights=x["mktcap_lag"]) })) .reset_index() )return market_dailymarket_factor_daily = compute_daily_market_factor(prices_daily)print(f"Daily market factor: {len(market_factor_daily):,} days")```### Kết hợp ba nhân tố ngày```{python}factors_ff3_daily = (factors_daily_smb_hml .merge(market_factor_daily, on="date", how="inner"))# Add risk-free rate (use monthly rate / 21 as daily approximation, or load actual daily rate)factors_ff3_daily["risk_free"] =0.04/252# Approximate daily risk-freeprint("Daily Fama-French Three Factors:")print(factors_ff3_daily.head(10))print("\nDaily Factor Summary Statistics:")print(factors_ff3_daily[["mkt_excess", "smb", "hml"]].describe().round(6))```### Tính năm nhân tố ngày```{python}# Step 1: Clean portfoliosportfolios_ff5_clean = portfolios_ff5[["symbol", "sorting_date", "portfolio_size", "portfolio_bm", "portfolio_op", "portfolio_inv"]].copy()portfolios_ff5_clean["sorting_date"] = pd.to_datetime(portfolios_ff5_clean["sorting_date"])# Step 2: Merge with daily pricesportfolios_daily_ff5 = prices_daily_with_sort.merge( portfolios_ff5_clean, on=["symbol", "sorting_date"], how="inner")print(f"FF5 Daily merged observations: {len(portfolios_daily_ff5):,}")``````{python}def compute_daily_ff5_factors(portfolios_daily):"""Compute daily Fama-French five factors."""# HML from B/M sorts portfolios_bm = (portfolios_daily .groupby(["portfolio_size", "portfolio_bm", "date"], observed=True) .apply(lambda x: pd.Series({"ret": np.average(x["ret_excess"], weights=x["mktcap_lag"]) })) .reset_index() ) factors_hml = (portfolios_bm .groupby("date") .apply(lambda x: pd.Series({"hml": (x.loc[x["portfolio_bm"] ==3, "ret"].mean() - x.loc[x["portfolio_bm"] ==1, "ret"].mean()) })) .reset_index() )# RMW from OP sorts portfolios_op = (portfolios_daily .groupby(["portfolio_size", "portfolio_op", "date"], observed=True) .apply(lambda x: pd.Series({"ret": np.average(x["ret_excess"], weights=x["mktcap_lag"]) })) .reset_index() ) factors_rmw = (portfolios_op .groupby("date") .apply(lambda x: pd.Series({"rmw": (x.loc[x["portfolio_op"] ==3, "ret"].mean() - x.loc[x["portfolio_op"] ==1, "ret"].mean()) })) .reset_index() )# CMA from INV sorts (note: low minus high) portfolios_inv = (portfolios_daily .groupby(["portfolio_size", "portfolio_inv", "date"], observed=True) .apply(lambda x: pd.Series({"ret": np.average(x["ret_excess"], weights=x["mktcap_lag"]) })) .reset_index() ) factors_cma = (portfolios_inv .groupby("date") .apply(lambda x: pd.Series({"cma": (x.loc[x["portfolio_inv"] ==1, "ret"].mean() - x.loc[x["portfolio_inv"] ==3, "ret"].mean()) })) .reset_index() )# SMB from all sorts all_portfolios = pd.concat([portfolios_bm, portfolios_op, portfolios_inv]) factors_smb = (all_portfolios .groupby("date") .apply(lambda x: pd.Series({"smb": (x.loc[x["portfolio_size"] ==1, "ret"].mean() - x.loc[x["portfolio_size"] ==2, "ret"].mean()) })) .reset_index() )# Combine factors = (factors_smb .merge(factors_hml, on="date", how="outer") .merge(factors_rmw, on="date", how="outer") .merge(factors_cma, on="date", how="outer") )return factors# Compute daily FF5 factorsfactors_daily_ff5 = compute_daily_ff5_factors(portfolios_daily_ff5)# Add market factorfactors_ff5_daily = (factors_daily_ff5 .merge(market_factor_daily, on="date", how="inner"))factors_ff5_daily["risk_free"] =0.04/252print("Daily Fama-French Five Factors:")print(factors_ff5_daily.head(10))print("\nDaily Five-Factor Summary Statistics:")print(factors_ff5_daily[["mkt_excess", "smb", "hml", "rmw", "cma"]].describe().round(6))```### Lưu các nhân tố ngày```{python}factors_ff3_daily.to_sql( name="factors_ff3_daily", con=tidy_finance, if_exists="replace", index=False)factors_ff5_daily.to_sql( name="factors_ff5_daily", con=tidy_finance, if_exists="replace", index=False)print("Daily factor data saved to database.")```## Kiểm chứng và chẩn đoán nhân tố```{python}# Verify all tables are in databaseprint("\n"+"="*50)print("DATABASE SUMMARY")print("="*50)tables = ["factors_ff3_monthly", "factors_ff5_monthly", "factors_ff3_daily", "factors_ff5_daily"]for table in tables: df = pd.read_sql_query(f"SELECT COUNT(*) as n FROM {table}", con=tidy_finance)print(f"{table}: {df['n'].iloc[0]:,} observations")# Correlation check: Monthly vs Daily (aggregated)print("\n"+"="*50)print("MONTHLY VS DAILY CONSISTENCY CHECK")print("="*50)factors_daily_agg = (factors_ff3_daily .assign(year_month=lambda x: x["date"].dt.to_period("M")) .groupby("year_month")[["mkt_excess", "smb", "hml"]] .sum() .reset_index())factors_monthly_check = (factors_ff3_monthly .assign(year_month=lambda x: x["date"].dt.to_period("M")))comparison = factors_monthly_check.merge( factors_daily_agg, on="year_month", suffixes=("_monthly", "_daily"))for factor in ["mkt_excess", "smb", "hml"]: corr = comparison[f"{factor}_monthly"].corr(comparison[f"{factor}_daily"])print(f"{factor}: Monthly-Daily correlation = {corr:.4f}")```### Lợi nhuận nhân tố tích lũyChúng ta trực quan hóa hiệu quả tích lũy của từng nhân tố để đánh giá xem các nhân tố có tạo ra phần bù có ý nghĩa theo thời gian hay không.```{python}#| label: fig-cumulative-factors#| out-width: "70%"#| fig-align: center#| fig-cap: "Lợi nhuận tích lũy của các nhân tố Fama-French cho thị trường Việt Nam. Hình thể hiện mức tăng trưởng của $1 đầu tư vào từng danh mục nhân tố."#| fig-alt: "Biểu đồ đường thể hiện lợi nhuận nhân tố tích lũy theo thời gian."# Compute cumulative returnsfactors_cumulative = (factors_ff5_monthly .set_index("date") [["mkt_excess", "smb", "hml", "rmw", "cma"]] .add(1) .cumprod())# Plotfig, ax = plt.subplots(figsize=(12, 6))factors_cumulative.plot(ax=ax)ax.set_title("Cumulative Factor Returns (Vietnam)")ax.set_xlabel("")ax.set_ylabel("Growth of $1")ax.legend(title="Factor")ax.axhline(y=1, color='gray', linestyle='--', alpha=0.5)plt.tight_layout()plt.show()```### Phần bù nhân tố trung bìnhChúng ta tính phần bù nhân tố trung bình quy năm và ý nghĩa thống kê của chúng.```{python}# Annualized average returns (monthly returns * 12)factor_premiums = (factors_ff5_monthly [["mkt_excess", "smb", "hml", "rmw", "cma"]] .mean() *12*100# Annualized percentage)# Standard errorsfactor_se = (factors_ff5_monthly [["mkt_excess", "smb", "hml", "rmw", "cma"]] .std() / np.sqrt(len(factors_ff5_monthly)) * np.sqrt(12) *100)# T-statisticsfactor_tstat = factor_premiums / factor_seprint("Annualized Factor Premiums (%):")print(factor_premiums.round(2))print("\nT-Statistics:")print(factor_tstat.round(2))```### So sánh các nhân tố tháng và ngàyChúng ta kiểm tra tính nhất quán giữa các nhân tố tháng và ngày bằng cách tính các tương quan.```{python}# Aggregate daily factors to monthly for comparisonfactors_daily_monthly = (factors_ff5_daily .assign(year_month=lambda x: x["date"].dt.to_period("M")) .groupby("year_month") [["mkt_excess", "smb", "hml", "rmw", "cma"]] .sum() # Sum daily returns to get monthly .reset_index())# Merge with actual monthly factorscomparison = (factors_ff5_monthly .assign(year_month=lambda x: x["date"].dt.to_period("M")) .merge( factors_daily_monthly, on="year_month", suffixes=("_monthly", "_daily") ))# Correlationsfor factor in ["mkt_excess", "smb", "hml", "rmw", "cma"]: corr = comparison[f"{factor}_monthly"].corr(comparison[f"{factor}_daily"])print(f"{factor}: Monthly-Daily correlation = {corr:.4f}")```## Những điểm chính1. **Giải thích các mô hình nhân tố**: Mô hình ba nhân tố Fama-French bổ sung các nhân tố quy mô (SMB) và giá trị (HML) vào CAPM, trong khi mô hình năm nhân tố bao gồm thêm các nhân tố khả năng sinh lời (RMW) và đầu tư (CMA).2. **Phương pháp xây dựng**: Các nhân tố được xây dựng thông qua các danh mục phân loại kép với sự chú ý cẩn thận đến thời điểm. Các danh mục được hình thành vào tháng Bảy, dùng dữ liệu kế toán của năm tài chính trước đó để bảo đảm thông tin đã được công bố công khai.3. **Phân loại độc lập và phụ thuộc**: Mô hình ba nhân tố dùng phân loại độc lập theo quy mô và tỷ số giá trị sổ sách trên giá trị thị trường, tạo ra lưới 2×3. Mô hình năm nhân tố dùng phân loại phụ thuộc, trong đó các đặc điểm được phân loại trong từng nhóm quy mô.4. **Lợi nhuận theo trọng số vốn hóa**: Lợi nhuận danh mục được tính theo trọng số vốn hóa với vốn hóa thị trường trễ một kỳ để tránh thiên lệch nhìn trước.5. **Nhân tố ngày**: Các nhân tố ngày dùng cùng việc gán danh mục hằng năm nhưng tính lợi nhuận ở tần suất ngày, cho phép các ứng dụng tần suất cao hơn như ước lượng beta ngày.6. **Nhân tố thị trường**: Nhân tố thị trường được tính độc lập bằng lợi nhuận theo trọng số vốn hóa của tất cả cổ phiếu trừ lãi suất phi rủi ro.7. **Kiểm chứng**: Chất lượng nhân tố có thể được đánh giá thông qua tính đơn điệu của đặc điểm, mức đa dạng hóa của danh mục, lợi nhuận tích lũy và tính nhất quán giữa tần suất ngày và tháng.8. **Điều chỉnh cho thị trường Việt Nam**: Trong khi bám theo phương pháp Fama-French gốc, chúng ta điều chỉnh cho phù hợp với đặc điểm thị trường Việt Nam, bao gồm chuẩn mực kế toán VAS, thời hạn báo cáo và đơn vị tiền tệ.