14  Các thước đo Thanh khoản và Vòng quay Giao dịch

Ghi chú

Trong chương này, chúng ta xây dựng một bộ thước đo thanh khoản toàn diện cho thị trường chứng khoán Việt Nam, đối chiếu chúng với nhau và với các chuẩn mực đã biết, kiểm định xem thanh khoản có được định giá trong mặt cắt ngang của lợi nhuận cổ phiếu hay không, khảo sát tính đồng biến (commonality) của thanh khoản, và phân tích cách các điều kiện thanh khoản thay đổi theo thời gian và giữa các trạng thái thị trường.

Thanh khoản (tức là khả năng giao dịch nhanh với chi phí thấp mà không làm dịch chuyển giá) có lẽ là cân nhắc thực tiễn quan trọng nhất đối với bất kỳ ai làm việc với dữ liệu chứng khoán Việt Nam. Một phần bù nhân tố trông hấp dẫn trong kiểm định ngược (backtest) không có ma sát có thể hoàn toàn không thể triển khai nếu các vị thế mua và bán khống tập trung vào những cổ phiếu kém thanh khoản, vốn có giá dịch chuyển bất lợi cho bạn khi bạn giao dịch. Ngược lại, một phần bù thanh khoản thực sự (tức là khoản bù đắp cho rủi ro cổ phiếu khó bán khi bạn cần bán) là một trong những hiện tượng bất thường vững chắc nhất và có nền tảng lý thuyết tốt nhất trong định giá tài sản.

Thách thức nằm ở chỗ thanh khoản vốn đa chiều và khó đo lường. Ở các thị trường phát triển với sổ lệnh giới hạn liên tục và báo cáo giao dịch dưới một giây, các nhà nghiên cứu có thể quan sát trực tiếp chênh lệch giá mua–bán, độ sâu thị trường và tác động giá. Ở Việt Nam, dữ liệu vi cấu trúc ở mức chi tiết này còn hạn chế: HOSE vận hành khớp lệnh định kỳ lúc mở cửa và đóng cửa với khớp lệnh liên tục ở giữa, bước giá thô so với mặt bằng giá, và nhiều cổ phiếu giao dịch thưa đến mức khái niệm “chênh lệch giá chào” chỉ có ý nghĩa vào những ngày cổ phiếu thực sự có giao dịch. Điều này buộc các nhà nghiên cứu phải dựa vào các thước đo xấp xỉ tần suất thấp được tính từ dữ liệu giá và khối lượng theo ngày.

Chương này xây dựng các thước đo xấp xỉ thanh khoản chính được dùng trong tài liệu học thuật, kiểm chứng chúng trong bối cảnh Việt Nam, và minh họa việc sử dụng chúng trong định giá tài sản và xây dựng danh mục đầu tư.

14.1 Nền tảng Lý thuyết

14.1.1 Vì sao Thanh khoản Quan trọng

Thanh khoản tác động đến giá tài sản qua ít nhất ba kênh:

  1. Hiệu ứng mức độ. Nhà đầu tư đòi hỏi được bù đắp cho chi phí giao dịch kỳ vọng. Amihud và Mendelson (1986) chỉ ra rằng các cổ phiếu có chênh lệch giá mua–bán cao hơn mang lại lợi nhuận kỳ vọng cao hơn, với phần bù là hàm tăng của thời gian nắm giữ của nhà đầu tư. Ở trạng thái cân bằng, các cổ phiếu kém thanh khoản phải đem lại lợi nhuận kỳ vọng cao hơn để bù đắp chi phí giao dịch khứ hồi cao hơn.
  2. Hiệu ứng rủi ro. Thanh khoản biến đổi theo thời gian và đồng biến giữa các cổ phiếu. Pástor và Stambaugh (2003) chỉ ra rằng các cổ phiếu có lợi nhuận nhạy hơn với các cú sốc thanh khoản tổng hợp mang lại lợi nhuận kỳ vọng cao hơn. Acharya và Pedersen (2005) chính thức hóa điều này trong một CAPM điều chỉnh theo thanh khoản, trong đó lợi nhuận đòi hỏi bao gồm phần bù cho rủi ro thanh khoản (tức là rủi ro cổ phiếu trở nên kém thanh khoản đúng vào lúc nhà đầu tư cần bán).
  3. Hiệu ứng đồng biến. Chordia, Roll, và Subrahmanyam (2000) ghi nhận rằng thanh khoản của từng cổ phiếu đồng biến mạnh với thanh khoản toàn thị trường. Brunnermeier và Pedersen (2009) giải thích điều này qua một “vòng xoáy thanh khoản”: khi giá trị tài sản giảm, các ràng buộc ký quỹ siết chặt, buộc các nhà đầu tư dùng đòn bẩy phải bán, làm giảm thanh khoản thị trường, từ đó đẩy giá xuống thấp hơn nữa. Cơ chế này đặc biệt phù hợp với Việt Nam, nơi nhà đầu tư cá nhân có tài khoản ký quỹ là nhóm giao dịch chiếm ưu thế.

14.1.2 Các Chiều của Thanh khoản

Kyle (1985) xác định ba chiều của thanh khoản:

  1. Độ chặt (tightness). Chi phí để đảo vị thế một cách nhanh chóng, được đo bằng chênh lệch giá mua–bán.
  2. Độ sâu (depth). Khối lượng có thể giao dịch mà không làm dịch chuyển giá, liên quan đến tác động giá.
  3. Khả năng phục hồi (resiliency). Tốc độ giá phục hồi sau các cú sốc dòng lệnh không mang thông tin.

Không có thước đo đơn lẻ nào nắm bắt được cả ba chiều. Goyenko, Holden, và Trzcinka (2009) và Fong, Holden, và Trzcinka (2017) đánh giá một cách hệ thống thước đo xấp xỉ tần suất thấp nào nắm bắt tốt nhất từng chiều bằng cách đối chiếu với các thước đo tần suất cao. Phát hiện chính của họ: thước đo Amihud (2002) nắm bắt tốt nhất chiều tác động giá, ước lượng Roll (1984) và chênh lệch Corwin và Schultz (2012) nắm bắt tốt nhất độ chặt, còn thước đo lợi nhuận bằng không của Lesmond, Ogden, và Trzcinka (1999) nắm bắt sự kết hợp giữa chi phí giao dịch và bất cân xứng thông tin. Riêng đối với các thị trường mới nổi, Fong, Holden, và Trzcinka (2017) khuyến nghị thước đo Amihud và Chênh lệch giá chào theo phần trăm lúc đóng cửa (Closing Percent Quoted Spread) là các thước đo xấp xỉ đáng tin cậy nhất.

14.2 Xây dựng Dữ liệu

Import libraries and configure environment
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import statsmodels.api as sm
from scipy import stats
from linearmodels.panel import PanelOLS
from linearmodels.asset_pricing import LinearFactorModel
import warnings
warnings.filterwarnings('ignore')

plt.rcParams.update({
    'figure.figsize': (12, 6),
    'figure.dpi': 150,
    'font.size': 11,
    'axes.spines.top': False,
    'axes.spines.right': False
})
Load daily and monthly data
from datacore import DataCoreClient

client = DataCoreClient()

# Daily trading data
daily = client.get_daily_prices(
    exchanges=['HOSE', 'HNX'],
    start_date='2008-01-01',
    end_date='2024-12-31',
    include_delisted=True,
    fields=[
        'ticker', 'date', 'open', 'high', 'low', 'close',
        'adjusted_close', 'volume', 'turnover_value',
        'market_cap', 'shares_outstanding', 'free_float_pct',
        'bid', 'ask', 'foreign_buy_volume', 'foreign_sell_volume'
    ]
)

# Monthly aggregates
monthly = client.get_monthly_returns(
    exchanges=['HOSE', 'HNX'],
    start_date='2008-01-01',
    end_date='2024-12-31',
    include_delisted=True,
    fields=[
        'ticker', 'month_end', 'monthly_return', 'market_cap',
        'volume_avg_20d', 'turnover_value_avg_20d',
        'n_trading_days', 'n_zero_volume_days'
    ]
)

# Firm characteristics for cross-sectional tests
fundamentals = client.get_fundamentals(
    exchanges=['HOSE', 'HNX'],
    start_date='2008-01-01',
    end_date='2024-12-31',
    include_delisted=True,
    frequency='annual',
    fields=[
        'ticker', 'fiscal_year', 'total_assets', 'total_equity',
        'net_income', 'revenue', 'book_equity'
    ]
)

# Factor returns for asset pricing tests
factors = client.get_factor_returns(
    market='vietnam',
    start_date='2008-01-01',
    end_date='2024-12-31',
    factors=['mkt_excess', 'smb', 'hml', 'wml']
)

daily['date'] = pd.to_datetime(daily['date'])
daily = daily.sort_values(['ticker', 'date'])

print(f"Daily observations: {daily.shape[0]:,}")
print(f"Monthly observations: {monthly.shape[0]:,}")
print(f"Unique tickers: {daily['ticker'].nunique()}")
Compute daily returns and basic trading statistics
# Daily returns
daily['daily_return'] = (
    daily.groupby('ticker')['adjusted_close'].pct_change()
)
daily['abs_return'] = daily['daily_return'].abs()
daily['log_return'] = np.log(
    daily['adjusted_close'] / daily.groupby('ticker')['adjusted_close'].shift(1)
)

# Turnover ratio (shares traded / shares outstanding)
daily['turnover_ratio'] = daily['volume'] / daily['shares_outstanding']

# Zero indicators
daily['zero_return'] = (daily['daily_return'] == 0).astype(int)
daily['zero_volume'] = (daily['volume'] == 0).astype(int)

# VND turnover (in billions)
daily['turnover_vnd_bn'] = daily['turnover_value'] / 1e9

print("Daily Return Summary:")
print(daily['daily_return'].describe().round(6))
print(f"\nZero-return days: {daily['zero_return'].mean():.1%}")
print(f"Zero-volume days: {daily['zero_volume'].mean():.1%}")

14.3 Xây dựng các Thước đo Thanh khoản

Chúng ta xây dựng bảy thước đo xấp xỉ thanh khoản bao trùm các chiều độ chặt, độ sâu và khả năng phục hồi. Mỗi thước đo được tính ở cấp công ty–tháng, tạo ra một bảng dữ liệu có thể ghép với dữ liệu lợi nhuận theo tháng cho các kiểm định mặt cắt ngang.

14.3.1 Tỷ số Phi thanh khoản Amihud

Thước đo phi thanh khoản Amihud (2002) là tỷ số giữa giá trị tuyệt đối của lợi nhuận theo ngày và khối lượng giao dịch theo ngày (tính bằng VND):

\[ \text{ILLIQ}_{i,m} = \frac{1}{D_{i,m}} \sum_{d=1}^{D_{i,m}} \frac{|R_{i,d}|}{\text{DVOL}_{i,d}} \tag{14.1}\]

trong đó \(|R_{i,d}|\) là giá trị tuyệt đối của lợi nhuận theo ngày, \(\text{DVOL}_{i,d}\) là khối lượng giao dịch tính bằng VND trong ngày \(d\), và \(D_{i,m}\) là số ngày giao dịch có khối lượng dương trong tháng \(m\). Giá trị càng cao cho thấy mức phi thanh khoản càng lớn.

Thước đo Amihud nắm bắt chiều tác động giá của thanh khoản. Nó dựa trên mô hình Kyle (1985), trong đó tham số \(\lambda\) (lambda của Kyle) đo tác động giá của dòng lệnh: \(\Delta p = \lambda \cdot Q\). Tỷ số Amihud là phiên bản tương tự của \(\lambda\) ở tần suất theo ngày.

Compute monthly Amihud illiquidity ratio
def compute_amihud(daily_df, min_days=10):
    """
    Compute the Amihud (2002) illiquidity ratio at the firm-month level.
    
    Excludes zero-volume days. Requires at least min_days observations
    with positive volume per firm-month.
    """
    df = daily_df[daily_df['volume'] > 0].copy()
    df['month'] = df['date'].dt.to_period('M')
    
    # |Return| / VND Volume
    df['illiq_daily'] = df['abs_return'] / df['turnover_value']
    
    # Remove extreme outliers (top 0.1% within each month)
    df['illiq_daily'] = df.groupby('month')['illiq_daily'].transform(
        lambda x: x.clip(upper=x.quantile(0.999))
    )
    
    # Aggregate to firm-month
    amihud = (
        df.groupby(['ticker', 'month'])
        .agg(
            amihud_raw=('illiq_daily', 'mean'),
            n_positive_vol_days=('illiq_daily', 'count')
        )
        .reset_index()
    )
    
    # Filter: require minimum trading days
    amihud = amihud[amihud['n_positive_vol_days'] >= min_days]
    
    # Log transform (raw Amihud is heavily right-skewed)
    amihud['amihud'] = np.log(1 + amihud['amihud_raw'] * 1e6)
    
    # Convert period to timestamp for merging
    amihud['month_end'] = amihud['month'].dt.to_timestamp('M')
    
    return amihud[['ticker', 'month_end', 'amihud', 'amihud_raw',
                    'n_positive_vol_days']]

amihud_monthly = compute_amihud(daily)
print(f"Amihud observations: {len(amihud_monthly):,}")
print(f"\nLog Amihud distribution:")
print(amihud_monthly['amihud'].describe().round(3))

14.3.1.1 Định nghĩa Gốc năm 2002

Phương trình 37.2 là dạng hiện đại của thước đo, không phải dạng mà Amihud (2002) thực sự ước lượng. Dạng gốc tính theo năm chứ không phải theo tháng:

\[ \text{ILLIQ}_{i,y} = \frac{1}{D_{i,y}}\sum_{d=1}^{D_{i,y}} \frac{|R_{i,y,d}|}{\text{VOLD}_{i,y,d}} \tag{14.2}\]

trong đó \(\text{VOLD}_{i,y,d}\) là giá trị giao dịch bằng đô la trong ngày \(d\) của năm \(y\) và \(D_{i,y}\) là số ngày trong năm \(y\) có đủ cả dữ liệu lợi nhuận lẫn khối lượng. Những khác biệt so với Phương trình 37.2 không chỉ là hình thức, và mỗi khác biệt là một quyết định mô hình hóa mà các tác giả sau này đã lặng lẽ thay đổi:

  1. Theo năm, không theo tháng. Đối tượng quan tâm của Amihud là mức phi thanh khoản kỳ vọng, mà ông mô hình hóa như một đại lượng biến đổi chậm. Ông hồi quy lợi nhuận theo tháng trong năm \(y\) trên \(\text{ILLIQ}\) đo trong năm \(y-1\), nên thước đo là một đặc điểm công ty có tính dai dẳng chứ không phải một biến trạng thái theo tháng. Quy ước theo tháng hiện đang thống trị trong tài liệu đánh đổi tính dai dẳng để lấy độ nhạy, và chịu nhiều nhiễu đo lường hơn đáng kể trên mỗi quan sát: lấy trung bình khoảng 21 tỷ số theo ngày thay vì 250 làm sai số lấy mẫu của giá trị trung bình tăng khoảng \(\sqrt{250/21} \approx 3.5\) lần.

  2. Chỉ NYSE. Amihud cố ý loại NASDAQ, vì các giao dịch giữa các nhà tạo lập trên thị trường qua nhà tạo lập (dealer market) tính trùng khối lượng và sẽ làm nhiễm bẩn mẫu số. Điểm tương ứng ở Việt Nam không phải là thị trường qua nhà tạo lập mà là phân khúc giao dịch thỏa thuận (put-through, giao dịch khối lượng lớn thương lượng) và, trên UPCoM, hoạt động giao dịch cực kỳ rời rạc; cả hai đều làm méo \(\text{DVOL}\) theo những cách mà thiết kế gốc được xây dựng để tránh.

  3. Các bộ lọc. Ông yêu cầu có hơn 200 ngày dữ liệu trong năm, giá cuối năm trên $5, và có số cổ phiếu đang lưu hành, đồng thời cắt bỏ 1% cao nhất và thấp nhất của mặt cắt ngang \(\text{ILLIQ}\) theo năm như các giá trị ngoại lai. Bộ lọc giá loại bỏ các cổ phiếu mà tính rời rạc của bước giá chi phối tử số, một bộ lọc có tác động thực sự ở Việt Nam, nơi một phần lớn các mã niêm yết giao dịch dưới 10,000 VND với bước giá 10 VND.

  4. Hệ số quy đổi. Amihud nhân với \(10^6\), đây là một quy ước đơn vị gắn với đô la, không phải thuộc tính của ước lượng. Chúng ta sẽ quay lại điểm này bên dưới, vì đây là nguồn phổ biến nhất khiến các con số Amihud trong tài liệu không thể so sánh với nhau.

  5. Điều chỉnh theo trung bình. Đây là thành phần của bài báo gốc bị bỏ qua thường xuyên nhất và là thành phần quan trọng nhất đối với Việt Nam. Vì giá trị giao dịch bằng đô la tăng theo xu hướng dài hạn cùng với lạm phát, tăng trưởng thị trường và tỷ lệ vòng quay tăng, \(\text{ILLIQ}\) thô mang một xu hướng giảm cơ học không liên quan gì đến việc thanh khoản được cải thiện. Do đó Amihud cũng làm việc với thước đo đã điều chỉnh theo trung bình

    \[ \text{ILLIQMA}_{i,y} = \frac{\text{ILLIQ}_{i,y}}{\text{AILLIQ}_{y}}, \qquad \text{AILLIQ}_{y} = \frac{1}{N_y}\sum_{i=1}^{N_y}\text{ILLIQ}_{i,y} \tag{14.3}\]

    vốn không có đơn vị và đã được khử xu hướng theo cấu trúc. Trong một thị trường mà giá trị giao dịch bằng VND tăng gấp cả một bậc độ lớn trong giai đoạn mẫu, việc dùng \(\text{ILLIQ}\) thô trong bất kỳ đặc tả gộp (pooled) hay chuỗi thời gian nào mà không có hiệu ứng cố định theo thời kỳ sẽ cho thấy một “sự cải thiện thanh khoản” thực chất là sản phẩm giả tạo của mẫu số.

Hai kết quả chính của ông đáng được tách bạch, vì chúng thường bị nhập làm một. Trong mặt cắt ngang, mức phi thanh khoản kỳ vọng được định giá dương: các cổ phiếu có \(\text{ILLIQMA}\) năm trước cao hơn mang lại lợi nhuận sau đó cao hơn. Trong chuỗi thời gian, mức phi thanh khoản không được kỳ vọng — cú sốc (innovation) từ một AR(1) khớp với \(\text{AILLIQ}_y\) — gắn với lợi nhuận đồng thời âm, vì một cú sốc thanh khoản được tính ngay vào giá dưới dạng giá giảm dù nó làm tăng lợi nhuận kỳ vọng trong tương lai. Một kiểm định trộn lẫn hai điều này sẽ thấy dấu của kết quả gây nhầm lẫn.

14.3.1.2 Tiền thân và Lý thuyết: Vì sao là Tỷ số Lợi nhuận trên Khối lượng

Thước đo này không xuất hiện từ hư không. Amihud và Mendelson (1986) đã chỉ ra, cả về lý thuyết lẫn thực nghiệm, rằng lợi nhuận kỳ vọng tăng theo chênh lệch giá mua–bán chào, vì nhà đầu tư phải được bù đắp cho chi phí của một vòng khứ hồi. Vấn đề của kết quả đó là dữ liệu: chênh lệch giá chào đòi hỏi lịch sử các báo giá, vốn không tồn tại ở hầu hết các thị trường và trong phần lớn lịch sử. Thước đo năm 2002 được hiểu tốt nhất như một câu trả lời cho câu hỏi “chỉ từ lợi nhuận và khối lượng theo ngày, ta có thể tính được đại lượng nào hành xử như một chi phí giao dịch?” — cũng chính là câu hỏi thúc đẩy các ước lượng Roll, Corwin–Schultz và Lesmond được phát triển ở những phần khác của chương này. Amihud, Mendelson, và Pedersen (2005) tổng quan tài liệu phát sinh từ đó.

Cơ sở lý thuyết đến từ Kyle (1985). Trong mô hình của Kyle, tác động giá của một dòng lệnh có dấu \(Q\) là \(\Delta p = \lambda Q\), nên \(\lambda = |\Delta p| / |Q|\) đúng là một tỷ số lợi nhuận trên khối lượng. Phương trình 37.2 là tỷ số đó được tính với dòng lệnh không dấu, là tất cả những gì dữ liệu theo ngày cung cấp. Brennan và Subrahmanyam (1996) chỉ ra trực tiếp rằng các ước lượng tác động giá xây dựng từ dòng lệnh có dấu trong phiên mang theo một phần bù lợi nhuận, còn Goyenko, Holden, và Trzcinka (2009) và Goyenko và Ukhov (2009) về sau xác lập rằng tỷ số Amihud là một thước đo xấp xỉ tần suất thấp tốt cho các chuẩn tác động giá tần suất cao đó. Fong, Holden, và Trzcinka (2017) mở rộng cuộc đua này sang dữ liệu toàn cầu và thấy nó hoạt động tốt bên ngoài Hoa Kỳ, đó là lý do nó là công cụ chủ lực trong nghiên cứu thị trường mới nổi và cũng là lý do chương này dựa nhiều vào nó.

14.3.1.3 Dòng dõi các Biến thể

Bảng 14.1 tập hợp các phép biến đổi đang được dùng phổ biến. Tất cả đều đơn điệu theo \(\text{ILLIQ}\) thô trong cùng một thời kỳ, điều này có một hệ quả quan trọng sẽ được phát triển bên dưới.

Bảng 14.1: Các phép biến đổi của tỷ số phi thanh khoản Amihud đang được dùng phổ biến. Tất cả đều tăng nghiêm ngặt theo \(\text{ILLIQ}\) thô trong cùng một thời kỳ.
Biến thể Định nghĩa Nguồn gốc / cách dùng điển hình
Thô Trung bình của \(\lvert R_d\rvert / \text{DVOL}_d\) Chính ước lượng; hầu như không bao giờ dùng trực tiếp
Amihud đã quy đổi \(\text{ILLIQ}\times 10^6\) Amihud (2002), khối lượng bằng đô la
Điều chỉnh theo trung bình \(\text{ILLIQ}_{i}/\overline{\text{ILLIQ}}\) Amihud (2002); không có đơn vị, đã khử xu hướng (Phương trình 14.3)
Logarit \(\log(\text{ILLIQ})\) hoặc \(\log(1+\text{ILLIQ})\) Ổn định phương sai; quy ước dùng trong mã nguồn ở trên
Căn bậc hai \(\sqrt{\text{ILLIQ}}\) Hasbrouck (2009); đơn vị gần hơn với chi phí hiệu dụng
Chi phí chuẩn hóa \(\min(a + b\,\text{ILLIQ}\,P^M_{t-1},\,\bar{c})\) Acharya và Pedersen (2005); ánh xạ sang chi phí theo phần trăm, có chặn trên
Thứ hạng trong thời kỳ Phân vị của \(\text{ILLIQ}\) trong thời kỳ \(t\) Karolyi, Lee, và Dijk (2012), Amihud và c.s. (2015); so sánh được giữa các thị trường

Hai trong số này đáng được bàn kỹ hơn. Phép chuẩn hóa của Acharya và Pedersen (2005) tồn tại vì mô hình của họ đòi hỏi mức phi thanh khoản được biểu thị dưới dạng chi phí giao dịch theo phần trăm, để có thể trừ khỏi lợi nhuận. Hiệu chỉnh của họ \(\min(0.25 + 0.30\,\text{ILLIQ}\,P^M_{t-1},\,30)\) co giãn theo mức tăng của danh mục thị trường, hấp thụ đúng xu hướng khối lượng mà \(\text{ILLIQMA}\) cũng hấp thụ, và chặn kết quả ở mức 30% để đuôi cực đoan không chi phối. Các hằng số được khớp với dữ liệu giá trị giao dịch bằng đô la Mỹ trong một giai đoạn cụ thể và không thể chuyển sang VND. Mã nguồn bên dưới cho thấy công thức thất bại theo cả hai hướng chỉ tùy thuộc vào đơn vị khối lượng: với khối lượng tính bằng VND, mọi cổ phiếu đều được gán chi phí hệ số chặn 0.25% với độ phân tán mặt cắt ngang bằng không, và với khối lượng tính bằng tỷ VND, mọi cổ phiếu đều bị ghim ở mức chặn 30%, lại với độ phân tán bằng không. Chỉ có một đơn vị trung gian tình cờ tạo ra biến thiên, và việc chọn đơn vị đó không có gì thuộc về kinh tế. Phép biến đổi thứ hạng trong thời kỳ được dùng trong tài liệu thanh khoản quốc tế (Karolyi, Lee, và Dijk 2012; Amihud và c.s. 2015) tồn tại chính vì lý do này. Đây là phép biến đổi duy nhất trong bảng bất biến đối với đơn vị tiền tệ, đơn vị khối lượng và giai đoạn.

Một cải tiến nữa đáng biết là nghiên cứu của Amihud và Noh (2021), trong đó các tác giả xem xét lại các kết quả gốc bằng một nhân tố kém thanh khoản trừ thanh khoản có thể giao dịch được và một đặc tả có điều kiện. Cách xây dựng của họ là công trình đã công bố gần nhất với nhân tố thanh khoản được xây dựng ở cuối chương này.

14.3.1.4 Việc Quy đổi Không Vô hại

Đoạn mã sau đây độc lập và có thể chạy được. Nó xây dựng một bảng dữ liệu theo ngày tổng hợp với mức phi thanh khoản thật đã biết và không đổi rồi cho thấy việc chọn biến thể tác động thế nào đến các con số.

All Amihud variants, and what scaling does to them
def amihud_variants(daily, ret="ret", dvol="dvol", scale=1e6, min_days=15):
    """
    Compute the Amihud (2002) ratio and its common transformations from a
    daily panel with columns [ticker, date, ret, dvol].

    `dvol` is currency volume in whatever unit the caller supplies; `scale`
    is Amihud's units convention, which is *not* unit-free (see below).
    """
    df = daily.loc[daily[dvol] > 0].copy()
    df["period"] = df["date"].dt.to_period("M")
    df["illiq_d"] = df[ret].abs() / df[dvol]

    p = (df.groupby(["ticker", "period"])
           .agg(illiq=("illiq_d", "mean"), n_days=("illiq_d", "size"))
           .reset_index())
    p = p.loc[p["n_days"] >= min_days].copy()

    p["illiq_scaled"] = p["illiq"] * scale                    # Amihud's x10^6
    p["illiq_ma"] = p["illiq"] / p.groupby("period")["illiq"].transform("mean")
    p["log_illiq"] = np.log(p["illiq_scaled"])                # variance stabilizing
    p["sqrt_illiq"] = np.sqrt(p["illiq_scaled"])              # Hasbrouck (2009)
    p["illiq_rank"] = p.groupby("period")["illiq"].rank(pct=True)
    return p


# A synthetic panel: 40 stocks, two years, TRUE illiquidity constant over time,
# but market-wide VND volume growing at 35% per year.
rng_v = np.random.default_rng(11)
dates_v = pd.bdate_range("2023-01-02", "2024-12-31")
tickers_v = [f"S{i:02d}" for i in range(40)]
level_v = rng_v.lognormal(np.log(3e9), 1.1, size=len(tickers_v))  # VND/day
years_v = (dates_v - dates_v[0]).days / 365.25

demo = pd.concat([
    pd.DataFrame({
        "ticker": t,
        "date": dates_v,
        "ret": rng_v.normal(0, 0.022, len(dates_v)),
        "dvol": L * np.exp(rng_v.normal(0, 0.6, len(dates_v))) * np.exp(0.35 * years_v),
    })
    for t, L in zip(tickers_v, level_v)
], ignore_index=True)

# (1) The x10^6 convention is a currency convention, not a normalization.
print("Median ILLIQ x 10^6, identical economics, three volume units:")
for unit, divisor in [("VND", 1.0), ("million VND", 1e6), ("USD @ 25,400", 25_400.0)]:
    med = amihud_variants(demo.assign(dvol=demo["dvol"] / divisor))["illiq_scaled"].median()
    print(f"  DVOL in {unit:<14s} {med:>12.6g}")

# (2) Mean adjustment removes the volume trend; raw scaling does not.
v = amihud_variants(demo)
ts = v.groupby("period")[["illiq_scaled", "illiq_ma"]].median()
first, last = ts.iloc[0], ts.iloc[-1]
print("\nTrue illiquidity is constant; volume grows 35%/yr. Median over the panel:")
for col, name in [("illiq_scaled", "ILLIQ x 10^6"), ("illiq_ma", "ILLIQMA     ")]:
    print(f"  {name}: {first[col]:>10.4g} -> {last[col]:>10.4g} "
          f"({100 * (last[col] / first[col] - 1):+.1f}%)")

# (3) Every variant is monotone within a period, so ranks are identical.
cols_v = ["illiq_scaled", "illiq_ma", "log_illiq", "sqrt_illiq", "illiq_rank"]
within = (v.groupby("period")[["illiq"] + cols_v]
            .corr(method="spearman").xs("illiq", level=1)[cols_v].mean())
print("\nMean within-period Spearman rank correlation with raw ILLIQ:")
print(within.round(4).to_string())

# (4) But the distributions they induce are very different.
print("\nPooled distribution shape:")
print(v[["illiq_scaled", "log_illiq", "sqrt_illiq"]]
      .agg(["skew", "kurt"]).round(3).to_string())

# (5) Acharya-Pedersen constants are calibrated to US dollar volume, so the
#     same formula degenerates in opposite directions depending on the unit.
print("\nAcharya-Pedersen cost min(0.25 + 0.30*ILLIQ*1e6, 30) by volume unit:")
for unit, divisor in [("VND", 1.0), ("million VND", 1e6), ("billion VND", 1e9)]:
    s = amihud_variants(demo.assign(dvol=demo["dvol"] / divisor))["illiq_scaled"]
    ap = np.minimum(0.25 + 0.30 * s, 30.0)
    print(f"  DVOL in {unit:<12s} mean={ap.mean():8.4f}  sd={ap.std():8.4f}  "
          f"at cap={100 * (ap >= 30.0).mean():5.1f}%")
Median ILLIQ x 10^6, identical economics, three volume units:
  DVOL in VND             5.40237e-06
  DVOL in million VND         5.40237
  DVOL in USD @ 25,400        0.13722

True illiquidity is constant; volume grows 35%/yr. Median over the panel:
  ILLIQ x 10^6:  7.324e-06 ->  4.068e-06 (-44.5%)
  ILLIQMA     :     0.6307 ->      0.641 (+1.6%)

Mean within-period Spearman rank correlation with raw ILLIQ:
illiq_scaled    1.0
illiq_ma        1.0
log_illiq       1.0
sqrt_illiq      1.0
illiq_rank      1.0

Pooled distribution shape:
      illiq_scaled  log_illiq  sqrt_illiq
skew         2.986      0.147       1.447
kurt        11.810     -0.360       2.519

Acharya-Pedersen cost min(0.25 + 0.30*ILLIQ*1e6, 30) by volume unit:
  DVOL in VND          mean=  0.2500  sd=  0.0000  at cap=  0.0%
  DVOL in million VND  mean=  2.8306  sd=  3.0728  at cap=  0.0%
  DVOL in billion VND  mean= 30.0000  sd=  0.0000  at cap=100.0%

Từ đó rút ra năm bài học, và chúng chính là lý do tiểu mục này tồn tại.

Hệ số \(\times 10^6\) không mang thông tin. Cùng một mức phi thanh khoản kinh tế được báo cáo ở khoảng \(5\times10^{-6}\), \(5\), hoặc \(0.13\) chỉ tùy thuộc vào việc khối lượng được ghi bằng VND, triệu VND hay USD. Do đó các con số “phi thanh khoản Amihud bằng 0.8” được công bố không thể so sánh giữa các bài báo trừ khi đơn vị khối lượng được nêu rõ, mà thường thì không. Hãy nêu rõ đơn vị; tốt hơn nữa, hãy báo cáo một phép biến đổi không phụ thuộc vào nó.

Mức thô di chuyển theo xu hướng của mẫu số. Trong mô phỏng, mức phi thanh khoản thật không đổi theo cấu trúc, vậy mà trung vị của thước đo thô giảm khoảng 45% trong hai năm chỉ vì khối lượng tăng. \(\text{ILLIQMA}\) thay đổi vài phần trăm, là nhiễu lấy mẫu. Giá trị giao dịch của Việt Nam tăng nhanh hơn nhiều so với 35% mỗi năm trong nhiều giai đoạn sau năm 2015, nên đây là mối quan tâm bậc nhất đối với mọi nhận định dạng “thanh khoản ở Việt Nam đã cải thiện trong giai đoạn mẫu”.

Đối với các phân loại trong cùng thời kỳ, việc chọn biến thể hoàn toàn không quan trọng. Mọi phép biến đổi trong Bảng 14.1 đều tăng nghiêm ngặt trong cùng một thời kỳ, nên tương quan hạng trung bình trong thời kỳ đúng bằng 1.0 và các phân loại danh mục ở phần sau của chương sẽ giống hệt nhau về mặt số học dù dùng biến thể nào. Điều này đáng biết vì nó giới hạn mức độ mà việc chọn lọc biến thể trong tài liệu có thể thực sự quan trọng: nó chỉ có thể quan trọng thông qua các đặc tả dùng mức — Fama-MacBeth gộp không có hiệu ứng cố định theo thời kỳ, hồi quy chuỗi thời gian, và mọi thứ so sánh độ lớn giữa các thời kỳ hoặc thị trường.

Phép biến đổi quan trọng vì lý do về phân phối. Độ lệch (skewness) gộp giảm từ khoảng 3.0 xuống 0.1 và độ nhọn vượt mức (excess kurtosis) từ khoảng 11.8 xuống \(-0.4\) dưới phép biến đổi logarit. Vì các hệ số góc Fama-MacBeth là hệ số OLS và OLS không vững trước một đuôi phải nặng như vậy, phép biến đổi logarit đang làm một việc thực sự trong các hồi quy mặt cắt ngang: không phải sửa các thứ hạng, mà là ngăn một số ít quan sát công ty–tháng quyết định hệ số góc.

Các hiệu chỉnh vay mượn không giữ được khi chuyển sang bối cảnh khác. Phép ánh xạ chi phí của Acharya và Pedersen (2005) là trường hợp rõ nhất. Với khối lượng tính bằng VND, nó gán cho mọi quan sát công ty–tháng giá trị hệ số chặn 0.25%, với độ lệch chuẩn mặt cắt ngang đúng bằng không; với khối lượng tính bằng tỷ VND, nó ghim tất cả ở mức chặn 30%, lại với độ phân tán bằng không. Trong cả hai trường hợp, nó đều không thất bại một cách ồn ào — nó trả về một chi phí theo phần trăm trông hợp lý nhưng hoàn toàn không mang thông tin nào về cổ phiếu nào kém thanh khoản. Mọi hằng số đã công bố dạng \(a + b\,\text{ILLIQ}\) đều kế thừa đơn vị của dữ liệu mà nó được khớp, và phải được ước lượng lại chứ không được sao chép.

Để áp dụng cùng hàm đó cho bảng dữ liệu thực của chương:

Apply the variant constructor to the Vietnamese daily panel
# `daily` carries turnover_value in VND; express it in billions so that the
# resulting ratio reads as "return per billion VND traded".
amihud_all = amihud_variants(
    daily.rename(columns={"turnover_value": "dvol"}).assign(
        dvol=lambda d: d["dvol"] / 1e9
    ),
    scale=100.0,          # report as PERCENT price move per billion VND
    min_days=10,
)
print(amihud_all[["illiq_scaled", "illiq_ma", "log_illiq"]].describe().round(4))

Đặt scale=100.0 với khối lượng tính bằng tỷ VND cho thước đo một cách đọc có thể giữ nguyên khi chuyển đổi: mức dịch chuyển giá theo phần trăm gắn với một tỷ VND giao dịch. Đây là quy ước chúng tôi khuyến nghị cho các nghiên cứu về Việt Nam, và nó luôn phải được nêu cùng với con số.

14.3.1.5 Điều Thực sự được Định giá: Phê bình của Lou–Shu

Có một phản biện sâu hơn đối với Phương trình 37.2 so với bất kỳ câu hỏi nào về việc quy đổi, và nó liên quan trực tiếp đến cải tiến tần suất cao được phát triển trong Mục 14.4.

Lou và Shu (2017) phân rã tỷ số Amihud thành phần quy cho mẫu số và phần quy cho tác động giá thực sự theo lợi nhuận trên mỗi đơn vị khối lượng. Phát hiện của họ gây khó chịu đối với cách diễn giải chuẩn: phần bù lợi nhuận gắn với \(\text{ILLIQ}\) được dẫn dắt bởi thành phần khối lượng giao dịch chứ không phải thành phần tác động giá. Nói thẳng ra, phân loại theo \(\text{ILLIQ}\) gần như là phân loại theo nghịch đảo của giá trị giao dịch bằng đô la, và phần bù mà nó đem lại có thể phản ánh bất cứ điều gì mà nghịch đảo giá trị giao dịch đó đại diện — sự nhận biết của nhà đầu tư, sự bị lãng quên, sự chú ý của nhà đầu tư cá nhân — chứ không phải khoản bù đắp cho tác động giá.

Điều này có hai hàm ý đối với cuốn sách.

Hàm ý thứ nhất là một cảnh báo. Ở những phần sau của chương này báo cáo phần bù thanh khoản từ các phân loại theo \(\text{ILLIQ}\), cách diễn giải đúng là “một phần bù gắn với đặc điểm Amihud”, chứ không phải “khoản bù đắp cho tác động giá”. Phân biệt hai điều này đòi hỏi phép phân rã, và đòi hỏi kiểm soát trực tiếp giá trị giao dịch bằng đô la.

Hàm ý thứ hai là một câu hỏi thực sự còn bỏ ngỏ mà ước lượng thực hiện (realized) có khả năng trả lời. Nội dung tác động giá của \(\text{ILLIQ}\) nằm ở tử số, và Mục 14.4 cho thấy tử số theo ngày \(|R_d|\) mang khoảng 76% sai số đo lường tương đối trong khi mẫu số \(\text{DVOL}_d\) được quan sát gần như không có sai số. Một cuộc đua giữa tử số đo với 76% nhiễu và mẫu số đo không nhiễu không phải là một phép thử công bằng xem thành phần nào quan trọng: sai số trong biến cổ điển (errors-in-variables) làm đóng góp ước lượng của tử số suy giảm về không bất kể vai trò kinh tế thật của nó. Liệu kết quả của Lou–Shu có còn đứng vững khi tử số được đo với độ chính xác cao hơn khoảng bảy lần của Lacava, Ranaldo, và Santucci de Magistris (2026) hay không, theo hiểu biết của chúng tôi, vẫn chưa được kiểm định. Chúng tôi đánh dấu đó là một cơ hội nghiên cứu chứ không phải một kết quả, và lưu ý rằng Việt Nam là nơi không thuận lợi để thử nghiệm, vì những lý do về dữ liệu trong phiên được nêu trong Mục 14.4.

14.3.1.6 Cuốn sách này Dùng Biến thể nào

Để cụ thể, các quy ước được áp dụng trong phần còn lại của chương này và của cuốn sách:

  1. Tần suất theo tháng, để nhất quán với bảng dữ liệu lợi nhuận theo tháng, chấp nhận sai số lấy mẫu tăng khoảng 3.5 lần so với cách xây dựng theo năm của Amihud.
  2. Khối lượng tính bằng tỷ VND, chỉ các ngày có khối lượng dương, tối thiểu mười ngày hợp lệ cho mỗi quan sát công ty–tháng.
  3. Biến đổi logarit cho công việc hồi quy, theo mã nguồn đi kèm Phương trình 37.2 ở trên, vì phân phối gộp quá lệch đối với OLS ở dạng mức.
  4. Thứ hạng hoặc nhóm ngũ phân vị trong tháng cho các phân loại danh mục, vốn như đã chỉ ra ở trên, bất biến đối với mọi lựa chọn khác trong danh sách này.
  5. Điều chỉnh theo trung bình bất cứ khi nào các mức được so sánh giữa các thời điểm, trên thực tế nghĩa là mọi đặc tả không có hiệu ứng cố định theo tháng.

Thước đo cũng được báo cáo không qua biến đổi trong các thống kê mô tả, để độc giả có thể quy đổi sang bất kỳ quy ước nào khác.

14.3.2 Ngày Lợi nhuận bằng Không (Thước đo Lesmond)

Lesmond, Ogden, và Trzcinka (1999) đề xuất dùng tỷ lệ các ngày có lợi nhuận bằng không làm thước đo chi phí giao dịch. Trực giác là nếu thay đổi giá trị thực trong một ngày nhỏ hơn chi phí giao dịch khứ hồi, một nhà đầu tư cận biên hợp lý sẽ không giao dịch, và lợi nhuận quan sát được sẽ bằng không. Do đó, tỷ lệ lợi nhuận bằng không là hàm tăng của chi phí giao dịch hiệu dụng.

Lesmond (2005) kiểm chứng thước đo này cho các thị trường mới nổi và thấy nó tương quan mạnh với các thước đo chi phí tường minh. Ở Việt Nam, nơi các ngày lợi nhuận bằng không rất phổ biến (như đã ghi nhận ở chương trước), thước đo này có ý nghĩa đặc biệt.

\[ \text{ZeroRet}_{i,m} = \frac{\text{Number of days with } R_{i,d} = 0}{D_{i,m}} \tag{14.4}\]

Compute monthly zero-return proportion
def compute_zero_return(daily_df):
    """
    Compute the Lesmond et al. (1999) zero-return measure
    at the firm-month level.
    """
    df = daily_df.copy()
    df['month'] = df['date'].dt.to_period('M')
    
    zero_ret = (
        df.groupby(['ticker', 'month'])
        .agg(
            n_days=('daily_return', 'count'),
            n_zero_return=('zero_return', 'sum'),
            n_zero_volume=('zero_volume', 'sum')
        )
        .reset_index()
    )
    
    zero_ret['zero_return_pct'] = (
        zero_ret['n_zero_return'] / zero_ret['n_days']
    )
    zero_ret['zero_volume_pct'] = (
        zero_ret['n_zero_volume'] / zero_ret['n_days']
    )
    
    zero_ret['month_end'] = zero_ret['month'].dt.to_timestamp('M')
    
    return zero_ret[['ticker', 'month_end', 'zero_return_pct',
                      'zero_volume_pct', 'n_days']]

zero_monthly = compute_zero_return(daily)
print(f"Zero-return observations: {len(zero_monthly):,}")
print(f"\nZero-return proportion distribution:")
print(zero_monthly['zero_return_pct'].describe().round(3))

14.3.3 Tỷ lệ Vòng quay Giao dịch

Tỷ lệ vòng quay cổ phiếu (tức là khối lượng theo ngày chia cho số cổ phiếu đang lưu hành) đo hoạt động giao dịch chứ không phải chi phí giao dịch. Datar, Naik, và Radcliffe (1998) dùng tỷ lệ vòng quay làm thước đo xấp xỉ thanh khoản và ghi nhận mối quan hệ mặt cắt ngang âm giữa tỷ lệ vòng quay và lợi nhuận kỳ vọng, phù hợp với giả thuyết phần bù thanh khoản.

\[ \text{Turn}_{i,m} = \frac{1}{D_{i,m}} \sum_{d=1}^{D_{i,m}} \frac{\text{Volume}_{i,d}}{\text{SharesOut}_{i,d}} \tag{14.5}\]

Compute monthly average turnover ratio
def compute_turnover(daily_df):
    """Compute average daily turnover ratio at the firm-month level."""
    df = daily_df.copy()
    df['month'] = df['date'].dt.to_period('M')
    
    turnover = (
        df.groupby(['ticker', 'month'])
        .agg(
            turnover_mean=('turnover_ratio', 'mean'),
            turnover_sum=('turnover_ratio', 'sum'),
            volume_mean=('volume', 'mean'),
            dvol_mean=('turnover_value', 'mean')
        )
        .reset_index()
    )
    
    # Log transform for cross-sectional normality
    turnover['log_turnover'] = np.log(
        turnover['turnover_mean'].clip(lower=1e-8)
    )
    turnover['log_dvol'] = np.log(
        turnover['dvol_mean'].clip(lower=1)
    )
    
    turnover['month_end'] = turnover['month'].dt.to_timestamp('M')
    
    return turnover[['ticker', 'month_end', 'turnover_mean',
                      'log_turnover', 'log_dvol']]

turnover_monthly = compute_turnover(daily)
print(f"Turnover observations: {len(turnover_monthly):,}")
print(f"\nLog turnover distribution:")
print(turnover_monthly['log_turnover'].describe().round(3))

14.3.4 Ước lượng Chênh lệch Roll

Roll (1984) suy ra một chênh lệch giá mua–bán ngầm định từ hiệp phương sai chuỗi của các thay đổi giá. Với các giả định rằng giá trị thực tuân theo bước ngẫu nhiên và giá quan sát được dao động giữa giá mua và giá bán:

\[ \text{Roll}_{i,m} = \begin{cases} 2\sqrt{-\text{Cov}(\Delta P_{i,d}, \Delta P_{i,d-1})} & \text{if } \text{Cov} < 0 \\ 0 & \text{if } \text{Cov} \geq 0 \end{cases} \tag{14.6}\]

trong đó \(\Delta P_{i,d} = P_{i,d} - P_{i,d-1}\). Thước đo này mang tính trực giác: dao động giữa giá mua và giá bán tạo ra tương quan chuỗi âm trong giá giao dịch, và độ lớn của tương quan âm này phản ánh chênh lệch giá.

Compute monthly Roll (1984) effective spread estimator
def compute_roll_spread(daily_df, min_days=15):
    """
    Compute the Roll (1984) effective spread from serial
    covariance of daily price changes.
    """
    df = daily_df.copy()
    df['month'] = df['date'].dt.to_period('M')
    df['price_change'] = df.groupby('ticker')['adjusted_close'].diff()
    df['price_change_lag'] = df.groupby('ticker')['price_change'].shift(1)
    
    def roll_estimate(group):
        if len(group) < min_days:
            return np.nan
        cov = group['price_change'].cov(group['price_change_lag'])
        if cov < 0:
            spread = 2 * np.sqrt(-cov)
            # Normalize by average price
            avg_price = group['adjusted_close'].mean()
            return spread / avg_price if avg_price > 0 else np.nan
        else:
            return 0.0
    
    roll = (
        df.dropna(subset=['price_change', 'price_change_lag'])
        .groupby(['ticker', 'month'])
        .apply(roll_estimate)
        .reset_index(name='roll_spread')
    )
    
    roll['month_end'] = roll['month'].dt.to_timestamp('M')
    
    return roll[['ticker', 'month_end', 'roll_spread']]

roll_monthly = compute_roll_spread(daily)
print(f"Roll spread observations: {len(roll_monthly):,}")
print(f"\nRoll spread distribution:")
print(roll_monthly['roll_spread'].describe().round(4))

14.3.5 Chênh lệch Cao–Thấp Corwin-Schultz

Corwin và Schultz (2012) ước lượng chênh lệch giá hiệu dụng từ giá cao nhất và thấp nhất theo ngày. Điểm then chốt là giá cao nhất và thấp nhất theo ngày chứa thông tin về cả biến động lẫn chênh lệch giá—giá cao nhất thường là một lệnh mua và giá thấp nhất là một lệnh bán, nên biên độ cao–thấp phản ánh cả biến động thực lẫn chênh lệch giá mua–bán. Bằng cách so sánh biên độ cao–thấp một ngày và hai ngày, phương pháp tách hai thành phần này:

\[ \hat{S}_{i,m} = \frac{2(e^{\hat{\alpha}} - 1)}{1 + e^{\hat{\alpha}}} \tag{14.7}\]

trong đó:

\[ \hat{\alpha} = \frac{\sqrt{2\hat{\beta}} - \sqrt{\hat{\beta}}}{3 - 2\sqrt{2}} - \sqrt{\frac{\hat{\gamma}}{3 - 2\sqrt{2}}} \tag{14.8}\]

với \(\hat{\beta}\) và \(\hat{\gamma}\) được tính từ các tỷ số cao–thấp lấy logarit trong một ngày và hai ngày.

Compute the Corwin-Schultz (2012) high-low spread estimator
def compute_corwin_schultz(daily_df, min_days=15):
    """
    Compute the Corwin and Schultz (2012) bid-ask spread
    estimator from daily high and low prices.
    """
    df = daily_df.copy()
    df['month'] = df['date'].dt.to_period('M')
    
    # Log high-low ratio
    df['log_hl'] = np.log(df['high'] / df['low'])
    df['log_hl_sq'] = df['log_hl'] ** 2
    
    # Two-day high and low
    df['high_2d'] = df.groupby('ticker')['high'].transform(
        lambda x: x.rolling(2).max()
    )
    df['low_2d'] = df.groupby('ticker')['low'].transform(
        lambda x: x.rolling(2).min()
    )
    df['log_hl_2d'] = np.log(df['high_2d'] / df['low_2d'])
    df['log_hl_2d_sq'] = df['log_hl_2d'] ** 2
    
    def cs_estimate(group):
        if len(group) < min_days:
            return np.nan
        
        beta = group['log_hl_sq'].mean() + group['log_hl_sq'].shift(1).mean()
        beta = group[['log_hl_sq']].rolling(2).sum().mean().values[0]
        gamma = group['log_hl_2d_sq'].mean()
        
        k = np.sqrt(2) - 1
        denom = 3 - 2 * np.sqrt(2)
        
        term1 = np.sqrt(max(beta, 0)) / denom
        if beta > 0:
            alpha_est = (np.sqrt(2 * beta) - np.sqrt(beta)) / denom
            alpha_est -= np.sqrt(max(gamma / denom, 0))
        else:
            alpha_est = 0
        
        # Spread estimate
        if alpha_est > 0:
            spread = 2 * (np.exp(alpha_est) - 1) / (1 + np.exp(alpha_est))
        else:
            spread = 0
        
        return min(spread, 0.20)  # Cap at 20% (sanity check)
    
    cs = (
        df.dropna(subset=['log_hl', 'log_hl_2d'])
        .groupby(['ticker', 'month'])
        .apply(cs_estimate)
        .reset_index(name='cs_spread')
    )
    
    cs['month_end'] = cs['month'].dt.to_timestamp('M')
    
    return cs[['ticker', 'month_end', 'cs_spread']]

cs_monthly = compute_corwin_schultz(daily)
print(f"Corwin-Schultz observations: {len(cs_monthly):,}")
print(f"\nCS spread distribution:")
print(cs_monthly['cs_spread'].describe().round(4))

14.3.6 Chênh lệch Giá mua–Giá bán Chào

Khi có sẵn các báo giá mua và bán, chênh lệch theo phần trăm của giá chào cung cấp một thước đo trực tiếp về độ chặt:

\[ \text{PQSPR}_{i,m} = \frac{1}{D_{i,m}} \sum_{d=1}^{D_{i,m}} \frac{\text{Ask}_{i,d} - \text{Bid}_{i,d}}{(\text{Ask}_{i,d} + \text{Bid}_{i,d})/2} \tag{14.9}\]

Compute monthly average quoted percentage spread
def compute_quoted_spread(daily_df):
    """Compute average quoted percentage spread at the firm-month level."""
    df = daily_df[
        (daily_df['bid'] > 0) & (daily_df['ask'] > 0) &
        (daily_df['ask'] >= daily_df['bid'])
    ].copy()
    
    df['month'] = df['date'].dt.to_period('M')
    df['pqspr'] = (
        (df['ask'] - df['bid']) / ((df['ask'] + df['bid']) / 2)
    )
    
    # Winsorize extreme values
    df['pqspr'] = df['pqspr'].clip(upper=df['pqspr'].quantile(0.999))
    
    spread = (
        df.groupby(['ticker', 'month'])
        .agg(
            quoted_spread=('pqspr', 'mean'),
            n_quotes=('pqspr', 'count')
        )
        .reset_index()
    )
    
    spread['month_end'] = spread['month'].dt.to_timestamp('M')
    
    return spread[['ticker', 'month_end', 'quoted_spread', 'n_quotes']]

quoted_monthly = compute_quoted_spread(daily)
print(f"Quoted spread observations: {len(quoted_monthly):,}")
print(f"\nQuoted spread distribution:")
print(quoted_monthly['quoted_spread'].describe().round(4))

14.3.7 Lambda của Kyle (Hồi quy Tác động Giá)

Chúng ta ước lượng lambda của Kyle (tức là tác động giá trên mỗi đơn vị dòng lệnh có dấu) bằng một hồi quy theo ngày:

\[ R_{i,d} = \alpha_i + \lambda_i \cdot \text{Sign}(R_{i,d}) \cdot \sqrt{\text{Volume}_{i,d}} + \varepsilon_{i,d} \tag{14.10}\]

Đây là một sự điều chỉnh của thước đo chi phí hiệu dụng Hasbrouck (2009). Hệ số \(\lambda_i\) đo giá dịch chuyển bao nhiêu trên mỗi đơn vị khối lượng (không dấu, lấy căn bậc hai).

Estimate Kyle’s lambda from daily price-volume regressions
def compute_kyle_lambda(daily_df, min_days=15):
    """
    Estimate Kyle's lambda (price impact per unit order flow)
    from daily return-on-signed-volume regressions.
    """
    df = daily_df[daily_df['volume'] > 0].copy()
    df['month'] = df['date'].dt.to_period('M')
    
    # Signed square-root volume (sign inferred from return)
    df['signed_sqrt_vol'] = (
        np.sign(df['daily_return']) * np.sqrt(df['volume'])
    )
    
    def estimate_lambda(group):
        if len(group) < min_days:
            return np.nan
        y = group['daily_return'].values
        x = group['signed_sqrt_vol'].values
        x = sm.add_constant(x)
        try:
            model = sm.OLS(y, x).fit()
            lam = model.params[1]
            return max(lam, 0)  # Lambda should be non-negative
        except Exception:
            return np.nan
    
    kyle = (
        df.groupby(['ticker', 'month'])
        .apply(estimate_lambda)
        .reset_index(name='kyle_lambda')
    )
    
    kyle['log_kyle'] = np.log(kyle['kyle_lambda'].clip(lower=1e-10))
    kyle['month_end'] = kyle['month'].dt.to_timestamp('M')
    
    return kyle[['ticker', 'month_end', 'kyle_lambda', 'log_kyle']]

kyle_monthly = compute_kyle_lambda(daily)
print(f"Kyle lambda observations: {len(kyle_monthly):,}")
print(f"\nLog Kyle lambda distribution:")
print(kyle_monthly['log_kyle'].describe().round(3))

14.4 Phi thanh khoản Thực hiện: Một Cải tiến Tần suất Cao cho Thước đo Amihud

14.4.1 Động cơ

Trong bảy thước đo xấp xỉ được xây dựng ở trên, tỷ số Amihud (2002) trong Phương trình 37.2 là thước đo mà cuốn sách này dựa vào nhiều nhất: nó là biến phân loại trong các phân loại danh mục và hồi quy Fama-MacBeth ở phần sau, là đầu vào cho các hồi quy về tính đồng biến, và là cơ sở của nhân tố thanh khoản có thể giao dịch được xây dựng ở cuối chương. Đây cũng chính là thước đo mà Fong, Holden, và Trzcinka (2017) khuyến nghị riêng cho các thị trường mới nổi. Vì vậy, rất đáng để đặt câu hỏi: nó đo lường chính xác đến mức nào điều mà nó tuyên bố đo lường?

Câu trả lời là: kém chính xác hơn nhiều so với mức độ phổ biến của nó gợi ý, và vì một lý do không hề liên quan đến mẫu số khối lượng. Tử số \(|R_{i,d}|\) là một quan sát đơn lẻ được dùng để tóm tắt toàn bộ biến động giá trong ngày. Xét như một thống kê, nó gần như không chệch đối với độ biến động của ngày nhưng lại nhiễu một cách phi thường — chúng ta sẽ định lượng điều này dưới đây ở mức khoảng 76% độ lệch chuẩn tương đối mỗi ngày. Mỗi đơn vị nhiễu đó lan truyền vào các phân loại danh mục dưới dạng suy giảm do sai số trong biến (errors-in-variables) cổ điển, làm phẳng các phần bù thanh khoản đo được về phía không.

Lacava, Ranaldo, và Santucci de Magistris (2026) giải quyết đúng vấn đề này. Họ định nghĩa phi thanh khoản thực hiện là tỷ số giữa độ biến động thực hiện và khối lượng giao dịch, và giới thiệu thước đo Amihud thực hiện, thay giá trị tuyệt đối của lợi nhuận theo ngày đơn lẻ bằng một ước lượng tần suất cao của cùng đại lượng đó. Họ chỉ ra rằng nó nắm bắt một cách vững chắc độ nhạy giá tích lũy trong phiên đối với giao dịch, ngay cả khi có độ biến động ngẫu nhiên, nhiễu vi cấu trúc và các bước nhảy thông tin; ghi nhận hiện tượng phân cụm không đồng nhất và hiệu ứng đòn bẩy trong chuỗi kết quả; và thấy rằng nó dự báo được lợi nhuận ngắn hạn. Phần này phát triển ước lượng theo ký hiệu của cuốn sách, dẫn giải vì sao mức cải thiện độ chính xác lại lớn đến vậy, và trình bày cái giá phải trả để triển khai nó trên HOSE.

14.4.2 Thiết lập và Ký hiệu

Let \(p_{i,t}\) denote the efficient log price of stock \(i\) at time \(t\), evolving over trading day \(d\) as an Itô semimartingale:

\[ dp_{i,t} = \mu_{i,t}\,dt + \sigma_{i,t}\,dW_{i,t} + dJ_{i,t} \tag{14.11}\]

where \(\sigma_{i,t}\) is a càdlàg stochastic volatility process, \(W_{i,t}\) a standard Brownian motion, and \(J_{i,t}\) a finite-activity jump process capturing discrete information arrivals. The object that summarizes the day’s price variation is the integrated variance

\[ \text{IV}_{i,d} = \int_{d-1}^{d} \sigma_{i,s}^2\,ds \tag{14.12}\]

Chia ngày giao dịch liên tục thành \(M\) khoảng và ký hiệu log lợi nhuận trong phiên là \(r_{i,d,j}\), \(j = 1,\dots,M\), sao cho lợi nhuận theo ngày thỏa mãn \(R_{i,d} = \sum_{j=1}^{M} r_{i,d,j}\). Như trước, \(\text{DVOL}_{i,d}\) là khối lượng giao dịch tính bằng VND trong ngày \(d\) và \(D_{i,m}\) là số ngày có khối lượng dương trong tháng \(m\).

Phương sai thực hiện và biến phân song lũy thừa (bipower variation) là

\[ \text{RV}_{i,d} = \sum_{j=1}^{M} r_{i,d,j}^2, \qquad \text{BV}_{i,d} = \frac{\pi}{2}\sum_{j=2}^{M} \left| r_{i,d,j} \right|\left| r_{i,d,j-1} \right| \tag{14.13}\]

Cả hai đều vững đối với \(\text{IV}_{i,d}\) khi \(M \to \infty\) trong trường hợp không có bước nhảy (Barndorff-Nielsen và Shephard 2002; Andersen và c.s. 2003); \(\text{BV}_{i,d}\) vẫn vững ngay cả khi có bước nhảy, và đó chính là điều khiến nó hữu ích ở phần dưới (Barndorff-Nielsen và Shephard 2004).

14.4.3 Các Giả định

(A1) Giá hiệu quả là nửa martingale. Log giá hiệu quả tuân theo Phương trình 14.11 với \(\int_{d-1}^{d}\sigma_{i,s}^4\,ds < \infty\).

Diễn giải. Giá được dẫn dắt bởi một quá trình khuếch tán với độ biến động có thể ngẫu nhiên và dai dẳng, cộng với các bước nhảy không thường xuyên. Đây là giả định yếu và nhìn chung không gây tranh cãi. Nó sẽ bị vi phạm bởi một quá trình giá có bước nhảy hoạt động vô hạn với cường độ đủ lớn, hoặc bởi một thị trường mỏng đến mức không một xấp xỉ thời gian liên tục nào còn có ý nghĩa — trường hợp sau là mối lo thực tế đối với các cổ phiếu vốn hóa nhỏ trên HOSE.

(A2) Nhiễu vi cấu trúc không đáng kể tại tần suất lấy mẫu. Giá quan sát được thỏa mãn \(\tilde{p}_{i,t} = p_{i,t} + u_{i,t}\), và khoảng lấy mẫu đủ thô để \(2M\,\mathbb{V}[u_{i,t}]\) là nhỏ so với \(\text{IV}_{i,d}\).

Diễn giải. Dao động giữa giá mua và giá bán, tính rời rạc của bước giá, và giá cũ không được cập nhật đều bơm nhiễu vào giá quan sát được. Vì \(\text{RV}\) cộng dồn lợi nhuận bình phương, nhiễu này tích lũy theo \(M\) thay vì triệt tiêu — lấy mẫu quá dày làm ước lượng tệ đi chứ không tốt lên. Trên HOSE, bước giá thô so với mặt bằng giá khiến ràng buộc này phát huy tác dụng sớm hơn so với dữ liệu Mỹ. Đây là giả định dễ bị vi phạm nhất trong thực tế, và chúng ta sẽ chỉ ra điều gì xảy ra khi nó bị vi phạm.

(A3) Không có bước nhảy, đối với thước đo Amihud thực hiện dạng thuần. \(J_{i,t} \equiv 0\) trong ngày \(d\).

Diễn giải. Một bất ngờ lợi nhuận hay một phiên chạm biên độ là một thay đổi giá lớn không phải do tác động giá của khối lượng giao dịch, nhưng \(\text{RV}\) vẫn tính nó như thể đúng là như vậy. Với tần suất cổ phiếu Việt Nam chạm biên độ \(\pm7\%\) của HOSE (xem thảo luận về biên độ giá trong chương vi cấu trúc thị trường), (A3) không hề vô hại ở đây. Nới lỏng giả định này chính là lý do chúng ta duy trì song song \(\text{BV}_{i,d}\).

(A4) Khối lượng đồng thời. \(\text{DVOL}_{i,d}\) được đo trên cùng cửa sổ thời gian với các lợi nhuận trong phiên.

Diễn giải. Nếu lợi nhuận được tính trên các phiên khớp lệnh liên tục nhưng khối lượng lại bao gồm cả các giao dịch thỏa thuận sau giờ đóng cửa, thì tử số và mẫu số đang nói về hai hoạt động giao dịch khác nhau. Trên HOSE, phiên thỏa thuận 14:45–15:00 khiến đây là một vấn đề hạch toán thực sự chứ không phải chi tiết kỹ thuật.

14.4.4 Ước lượng

Thước đo Amihud thực hiện thay \(|R_{i,d}|\) bằng một ước lượng tần suất cao đã được quy đổi tỷ lệ cho chính biến động giá của ngày đó:

\[ \text{RILLIQ}_{i,m} = \frac{1}{D_{i,m}}\sum_{d=1}^{D_{i,m}} \sqrt{\frac{2}{\pi}}\,\frac{\sqrt{\text{RV}_{i,d}}}{\text{DVOL}_{i,d}} \tag{14.14}\]

và phiên bản vững với bước nhảy thay \(\text{RV}_{i,d}\) bằng \(\text{BV}_{i,d}\):

\[ \text{RILLIQ}^{\text{BV}}_{i,m} = \frac{1}{D_{i,m}}\sum_{d=1}^{D_{i,m}} \sqrt{\frac{2}{\pi}}\,\frac{\sqrt{\text{BV}_{i,d}}}{\text{DVOL}_{i,d}} \tag{14.15}\]

Hằng số \(\sqrt{2/\pi} \approx 0.7979\) không phải để cho đẹp: nó đưa Phương trình 14.14 về đúng cùng thang đo với Phương trình 37.2, nhờ đó hai thước đo có thể so sánh trực tiếp về mặt số học và phiên bản thực hiện có thể thay thế trực tiếp ở mọi nơi cuốn sách đang dùng \(\text{ILLIQ}\).

14.4.5 Dẫn giải

Bước 1: \(|R_{i,d}|\) thực chất ước lượng cái gì. Điều kiện theo quỹ đạo độ biến động và bỏ qua xu hướng cùng bước nhảy, ta có \(R_{i,d} \mid \text{IV}_{i,d} \sim N(0, \text{IV}_{i,d})\). Đối với một biến bán chuẩn (half-normal),

\[ \mathbb{E}\!\left[|R_{i,d}| \,\middle|\, \text{IV}_{i,d}\right] = \sqrt{\tfrac{2}{\pi}}\sqrt{\text{IV}_{i,d}}, \qquad \mathbb{V}\!\left[|R_{i,d}| \,\middle|\, \text{IV}_{i,d}\right] = \left(1 - \tfrac{2}{\pi}\right)\text{IV}_{i,d} \tag{14.16}\]

Vậy \(|R_{i,d}|\) là ước lượng không chệch cho \(\sqrt{2/\pi}\sqrt{\text{IV}_{i,d}}\) — tử số của Amihud nhắm đúng mục tiêu. Tuy nhiên, độ lệch chuẩn tương đối của nó là

\[ \frac{\text{sd}\!\left(|R_{i,d}|\right)}{\mathbb{E}\!\left[|R_{i,d}|\right]} = \sqrt{\frac{1 - 2/\pi}{2/\pi}} = \sqrt{\frac{\pi}{2} - 1} \approx 0.7555 \tag{14.17}\]

Tử số Amihud của một ngày đơn lẻ mang theo khoảng 76% sai số đo lường tương đối. Đây là tính chất của phân phối bán chuẩn, không phải của một thị trường cụ thể nào — nó không thể giảm được nếu chỉ có một quan sát mỗi ngày.

Bước 2: \(\sqrt{\text{RV}_{i,d}}\) ước lượng cái gì. Dưới (A1)–(A3), phương sai thực hiện tuân theo định lý giới hạn trung tâm của Barndorff-Nielsen và Shephard (2002),

\[ \sqrt{M}\left(\text{RV}_{i,d} - \text{IV}_{i,d}\right) \;\xrightarrow{d}\; N\!\left(0,\; 2\,\text{IQ}_{i,d}\right), \qquad \text{IQ}_{i,d} = \int_{d-1}^{d}\sigma_{i,s}^4\,ds \tag{14.18}\]

Áp dụng phương pháp delta cho \(g(x) = \sqrt{x}\), với \(g'(\text{IV}) = 1/(2\sqrt{\text{IV}})\),

\[ \sqrt{M}\left(\sqrt{\text{RV}_{i,d}} - \sqrt{\text{IV}_{i,d}}\right) \;\xrightarrow{d}\; N\!\left(0,\; \frac{\text{IQ}_{i,d}}{2\,\text{IV}_{i,d}}\right) \tag{14.19}\]

Xét trường hợp riêng độ biến động không đổi cục bộ trong ngày (\(\text{IV} = \sigma^2\), \(\text{IQ} = \sigma^4\)), ta được độ lệch chuẩn tương đối

\[ \frac{\text{sd}\!\left(\sqrt{\text{RV}_{i,d}}\right)}{\mathbb{E}\!\left[\sqrt{\text{RV}_{i,d}}\right]} \approx \frac{1}{\sqrt{2M}} \tag{14.20}\]

Bước 3: So sánh. Chia Phương trình 14.17 cho Phương trình 14.20, tử số thực hiện chính xác hơn theo hệ số \(\sqrt{2M\left(\pi/2 - 1\right)}\). Các phiên khớp lệnh liên tục của HOSE diễn ra từ 09:15–11:30 và 13:00–14:30, tức 225 phút, nên lấy mẫu năm phút cho \(M = 45\):

Bảng 14.2: Sai số đo lường giải tích của tử số Amihud theo tần suất lấy mẫu, từ Phương trình 14.17 và Phương trình 14.20. Dòng 1 phút không phải là khuyến nghị — xem giả định (A2) và phần mô phỏng bên dưới.
Tần suất lấy mẫu \(M\) Độ lệch chuẩn tương đối của tử số Cải thiện so với \(\lvert R_d\rvert\)
Đóng cửa sang đóng cửa theo ngày (\(\lvert R_d \rvert\)) 1 0.7555 —
15 phút 15 0.1826 4.1\(\times\)
5 phút 45 0.1054 7.2\(\times\)
1 phút 225 0.0471 16.0\(\times\)

Ở tần suất lấy mẫu năm phút, phương sai nhiễu trong tử số giảm khoảng 51 lần.

Bước 4: Vì sao điều này quan trọng đối với các kết quả của cuốn sách. Viết biến phân loại theo tháng dưới dạng giá trị thật cộng sai số, \(\widehat{x}_{i,m} = x_{i,m} + \eta_{i,m}\). Trong các phân loại thanh khoản đơn biến và các hồi quy Fama-MacBeth ở phần sau của chương, sai số trong biến cổ điển làm suy giảm hệ số góc ước lượng theo tỷ số tin cậy

\[ \text{plim}\;\frac{\widehat{\gamma}}{\gamma} = \frac{\mathbb{V}[x]}{\mathbb{V}[x] + \mathbb{V}[\eta]} \tag{14.21}\]

Giảm \(\mathbb{V}[\eta]\) khoảng 51\(\times\) sẽ đẩy tỷ số tin cậy tiến về một, điều này làm tăng phần bù thanh khoản đo được cùng thống kê \(t\) của nó. Hệ quả thực tiễn khá khó chịu và đáng nói thẳng: phần bù thanh khoản ước lượng bằng tỷ số Amihud theo ngày là một cận dưới. Một phần bù yếu hoặc không có ý nghĩa thống kê trong dữ liệu Việt Nam có thể phản ánh một biến phân loại nhiều nhiễu chứ không phải sự vắng mặt của phần bù.

14.4.6 Trực giác

Tỷ số Amihud đặt câu hỏi “giá đã dịch chuyển bao nhiêu trên mỗi đồng VND được giao dịch?” và trả lời bằng đúng một con số mà ngày hôm đó tình cờ mang lại — mức dịch chuyển từ mở cửa đến đóng cửa. Nhưng một ngày mà giá đi lên 4%, quay đầu giảm 5%, rồi đóng cửa thấp hơn 1% là một ngày có độ nhạy giá đáng kể đối với giao dịch, trong khi \(|R_d| = 1\%\) hầu như không ghi nhận được điều đó. Độ biến động thực hiện đo cả quỹ đạo thay vì chỉ hai đầu mút, nhờ vậy nắm bắt được đúng độ nhạy giá tích lũy trong phiên mà Amihud đang cố gắng xấp xỉ. Lấy trung bình \(M\) bình phương lợi nhuận trong phiên thay vì lấy một giá trị tuyệt đối của lợi nhuận theo ngày, xét về mặt thống kê, chính là khác biệt giữa một quan sát và \(M\) quan sát.

Điểm vướng là quỹ đạo chỉ quan sát được thông qua lớp vi cấu trúc. Đẩy \(M\) lên đủ cao thì ta không còn đo quỹ đạo giá hiệu quả nữa mà bắt đầu đo dao động giữa giá mua và giá bán. Toàn bộ nghệ thuật của ước lượng này nằm ở việc chọn \(M\) đủ lớn để có thêm độ chính xác và đủ nhỏ để giữ (A2) còn hiệu lực.

14.4.7 Thiết kế Mô phỏng

Thí nghiệm sau đây đối chiếu Phương trình 14.14 với Phương trình 37.2 trên cùng một quá trình sinh dữ liệu (DGP), rồi lần lượt phá vỡ (A2) và (A3) để làm lộ các kiểu thất bại. Đại lượng mục tiêu là mức phi thanh khoản thật theo ngày đã quy đổi \(\sqrt{2/\pi}\sqrt{\text{IV}_{i,d}}/\text{DVOL}_{i,d}\), nhờ đó độ chệch và RMSE được đo so với một đại lượng đã biết.

Monte Carlo: realized vs. classic Amihud under noise and jumps
rng = np.random.default_rng(20260812)

M_HOSE = 45  # 225 continuous minutes on HOSE / 5-minute sampling


def simulate_days(n_days, M, noise_sd=0.0, jump_prob=0.0, jump_sd=0.0):
    """
    Simulate intraday paths with stochastic volatility, optional iid
    microstructure noise in observed log prices, and optional information
    jumps. Returns the ingredients of both illiquidity estimators.
    """
    # Integrated variance: gamma-distributed, mean daily vol about 2.5%
    IV = rng.gamma(shape=4.0, scale=(0.025 ** 2) / 4.0, size=n_days)

    # Efficient intraday returns
    eff = rng.normal(0.0, 1.0, size=(n_days, M)) * np.sqrt(IV / M)[:, None]

    # Information jumps (violates A3)
    if jump_prob > 0:
        hit = rng.binomial(1, jump_prob / M, size=(n_days, M))
        eff = eff + hit * rng.normal(0.0, jump_sd, size=(n_days, M))

    # Efficient log-price path, then contaminate with noise (violates A2)
    P = np.cumsum(eff, axis=1)
    P_obs = P + rng.normal(0.0, noise_sd, size=(n_days, M)) if noise_sd > 0 else P

    # Observed intraday returns and the daily open-to-close return
    r = np.diff(P_obs, axis=1, prepend=0.0)
    R_d = P_obs[:, -1]

    RV = np.sum(r ** 2, axis=1)
    BV = (np.pi / 2) * np.sum(np.abs(r[:, 1:]) * np.abs(r[:, :-1]), axis=1)

    # VND turnover, lognormal around 5 billion VND
    DVOL = np.exp(rng.normal(np.log(5e9), 1.0, size=n_days))

    return dict(IV=IV, R_d=R_d, RV=RV, BV=BV, DVOL=DVOL)


def score(sim, label):
    """Bias, RMSE (both relative to mean truth) and rank correlation."""
    k = np.sqrt(2.0 / np.pi)
    truth = k * np.sqrt(sim['IV']) / sim['DVOL']

    estimators = {
        'Amihud |R|/DVOL': np.abs(sim['R_d']) / sim['DVOL'],
        'Realized Amihud': k * np.sqrt(sim['RV']) / sim['DVOL'],
        'Bipower Amihud': k * np.sqrt(np.maximum(sim['BV'], 0)) / sim['DVOL'],
    }

    rows = []
    for name, est in estimators.items():
        rows.append({
            'estimator': name,
            'rel_bias': (est - truth).mean() / truth.mean(),
            'rel_rmse': np.sqrt(((est - truth) ** 2).mean()) / truth.mean(),
            'rank_corr': stats.spearmanr(est, truth).statistic,
        })

    out = pd.DataFrame(rows).set_index('estimator')
    print(f"\n--- {label}")
    print(out.round(4))
    return out


# Baseline: assumptions hold
score(simulate_days(30_000, M_HOSE), "Baseline (A1-A4 hold), M=45")

# Failure case A: microstructure noise, and what oversampling does to it
score(simulate_days(30_000, M_HOSE, noise_sd=0.0015),
      "Noise sd=0.15%, 5-minute sampling (M=45)")
score(simulate_days(30_000, 225, noise_sd=0.0015),
      "Noise sd=0.15%, 1-minute sampling (M=225) -- oversampled")

# Failure case B: information jumps
score(simulate_days(30_000, M_HOSE, jump_prob=0.2, jump_sd=0.03),
      "Information jumps (~1 per 5 days, 3% size)")

--- Baseline (A1-A4 hold), M=45
                 rel_bias  rel_rmse  rank_corr
estimator                                     
Amihud |R|/DVOL    0.0057    1.2755     0.7043
Realized Amihud   -0.0046    0.1772     0.9941
Bipower Amihud    -0.0181    0.1978     0.9924

--- Noise sd=0.15%, 5-minute sampling (M=45)
                 rel_bias  rel_rmse  rank_corr
estimator                                     
Amihud |R|/DVOL    0.0025    1.3034     0.7019
Realized Amihud    0.1588    0.3329     0.9901
Bipower Amihud     0.1501    0.3493     0.9875

--- Noise sd=0.15%, 1-minute sampling (M=225) -- oversampled
                 rel_bias  rel_rmse  rank_corr
estimator                                     
Amihud |R|/DVOL    0.0070    1.3661     0.7107
Realized Amihud    0.6584    1.1145     0.9827
Bipower Amihud     0.6969    1.1837     0.9807

--- Information jumps (~1 per 5 days, 3% size)
                 rel_bias  rel_rmse  rank_corr
estimator                                     
Amihud |R|/DVOL    0.1190    1.4930     0.6929
Realized Amihud    0.0933    0.5300     0.9709
Bipower Amihud     0.0122    0.2429     0.9891
rel_bias rel_rmse rank_corr
estimator
Amihud |R|/DVOL 0.118971 1.492983 0.692885
Realized Amihud 0.093316 0.529996 0.970871
Bipower Amihud 0.012169 0.242949 0.989143

Ba kết quả xuất hiện, và mỗi kết quả có một đối ứng trực tiếp trong phần giải tích ở trên.

Thứ nhất, dưới (A1)–(A4), cả hai ước lượng đều gần như không chệch — tử số Amihud theo ngày, như Phương trình 14.16 đã nói, không chệch đối với mục tiêu đúng — nhưng độ chính xác của chúng khác nhau một trời một vực. RMSE tương đối giảm từ khoảng 1.28 xuống khoảng 0.18, một tỷ lệ khoảng 7.2, đúng bằng hệ số cải thiện mà Bảng 14.2 dự báo bằng giải tích cho \(M = 45\). Tương quan hạng với mức phi thanh khoản thật tăng từ khoảng 0.70 lên khoảng 0.99. Tương quan hạng là con số quan trọng nhất đối với cuốn sách này, vì nó quyết định việc một phân loại danh mục có xếp cổ phiếu vào đúng nhóm ngũ phân vị hay không: ở mức 0.70, một tỷ lệ đáng kể các cổ phiếu bị xếp sai chỉ vì biến phân loại.

Thứ hai, việc phá vỡ (A2) làm thước đo Amihud thực hiện chệch lên trên, và lấy mẫu ở một phút thay vì năm phút làm độ chệch tệ hơn khoảng bốn lần chứ không tốt hơn — khoảng +16% tại \(M = 45\) so với khoảng +66% tại \(M = 225\). Đây là bài học không hiển nhiên của toàn bộ phần này: khi có nhiễu, nhiều dữ liệu hơn được lấy mẫu dày hơn lại gây hại thực sự. Cũng lưu ý rằng thước đo Amihud theo ngày thuần túy gần như miễn nhiễm với vấn đề cụ thể này, vì một mức lợi nhuận từ mở cửa đến đóng cửa chỉ chứa một liều nhiễu. Độ vững trước nhiễu vi cấu trúc là chiều duy nhất mà thước đo cổ điển thắng.

Thứ ba, khi có bước nhảy, thước đo Amihud thực hiện dạng thuần kế thừa một độ chệch lên trên rõ rệt (khoảng +9%) trong khi phiên bản song lũy thừa trong Phương trình 14.15 vẫn gần như không chệch (khoảng +1%) và còn cho RMSE thấp nhất trong ba thước đo. Với việc cổ phiếu Việt Nam thường xuyên chạm biên độ giá, đây là cấu hình nên ưu tiên cho thị trường Việt Nam.

14.4.8 Tái lập trên Dữ liệu Việt Nam

Lacava, Ranaldo, và Santucci de Magistris (2026) sử dụng dữ liệu tần suất cao của Mỹ, nên không thể tái lập trực tiếp bằng bảng dữ liệu theo ngày đã lắp ráp ở phần trước của chương. Quan điểm trung thực cho cuốn sách này là thước đo Amihud thực hiện hiện nằm ngoài tầm với đối với phần lớn mặt cắt ngang HOSE, vì một lý do đáng nói rõ: độ chính xác của ước lượng được dẫn dắt bởi \(M\), tức số lượng lợi nhuận trong phiên thực sự khác không. Một cổ phiếu chỉ khớp lệnh vài lần mỗi phiên không có 45 lợi nhuận năm phút mang thông tin — nó có một lưới năm phút phần lớn được lấp đầy bằng giá cũ được điền tiếp (forward-fill), với các lợi nhuận bằng không mang tính cơ học không đóng góp gì cho \(\text{RV}\). Chương vi cấu trúc thị trường mô phỏng trực tiếp điều này (Mục 4.5.6) và đi đến một kết luận rõ ràng hơn người ta có thể đoán. Mức tăng độ chính xác được quyết định bởi số khoảng mà giá thực sự thay đổi chứ không phải số điểm trên lưới, nên nó suy giảm từ mức 7.2\(\times\) theo lý thuyết xuống dưới 2\(\times\) đối với một cổ phiếu giao dịch năm lần mỗi ngày. Độ chệch xuống đi kèm trong \(\text{RV}\) là có thật nhưng chỉ ở bậc hai so với điều đó, vì việc thô hóa lấy mẫu của một martingale không làm chệch phương sai thực hiện về kỳ vọng; điều mà tình trạng giá cũ loại bỏ là biến động trước giao dịch đầu tiên và sau giao dịch cuối cùng. Đây chính là vấn đề giao dịch mỏng đã thúc đẩy thước đo lợi nhuận bằng không trong Phương trình 14.4, và nó tác động lên thước đo Amihud thực hiện mạnh hơn so với phiên bản theo ngày.

Hệ quả thực tiễn là ước lượng này là một cải tiến dành cho nhóm thanh khoản cao của thị trường — đại khái là các cổ phiếu thuộc VN30 và các mã vốn hóa trung bình lớn hơn — chứ không phải vật thay thế cho Phương trình 37.2 trên toàn bộ bảng dữ liệu. Việc xây dựng nó cho Việt Nam đòi hỏi dữ liệu khớp lệnh (tick) trong phiên từ HOSE, vốn không nằm trong quy trình dữ liệu theo ngày được sử dụng ở những phần khác của chương.

14.4.9 Hướng dẫn Thực hành

  1. Lấy mẫu ở năm phút, không dày hơn. Với 225 phút khớp lệnh liên tục của HOSE, điều này cho \(M = 45\) và mức tăng độ chính xác 7.2\(\times\). Nên cưỡng lại cám dỗ hạ xuống một phút: Bảng 14.2 cho thấy mức cải thiện lý thuyết, nhưng (A2) sẽ đổ vỡ từ lâu trước khi bạn thu được nó. Năm phút không phải là một quy ước tùy tiện — đó là tần suất mà sự đánh đổi chệch–phương sai giữa sai số lấy mẫu và sự tích lũy nhiễu thường được chọn (Andersen và c.s. 2003), các tính toán lấy mẫu tối ưu của Aït-Sahalia, Mykland, và Zhang (2005) và Bandi và Russell (2008) cho kết quả quanh vùng đó đối với cổ phiếu Mỹ có thanh khoản cao, và Liu, Patton, và Sheppard (2015) thấy trên nhiều loại tài sản rằng rất ít thước đo thực hiện nào luôn vượt được \(\text{RV}\) năm phút. Vì HOSE mỏng hơn và bước giá thô hơn các thị trường mà những nghiên cứu đó khảo sát, khoảng tối ưu ở đây nếu có khác thì là dài hơn năm phút, không bao giờ ngắn hơn.
  2. Sàng lọc theo tần suất giao dịch trước khi tính. Yêu cầu một số lượng tối thiểu các khoảng có giao dịch thực sự xảy ra — chứ không đơn thuần là có giá — và quay về Phương trình 37.2 khi dưới ngưỡng đó. Hãy báo cáo những cổ phiếu nào phải dùng phương án dự phòng.
  3. Xử lý giờ nghỉ trưa và các phiên khớp lệnh định kỳ một cách tường minh. Khoảng 11:30–13:00 không phải là một khoảng tính lợi nhuận, và giá ATO cùng ATC đến từ khớp lệnh định kỳ chứ không phải khớp lệnh liên tục. Hãy tính \(\text{RV}\) trong từng phiên liên tục rồi cộng lại, thay vì lấy sai phân xuyên qua giờ nghỉ hoặc xuyên qua các phiên định kỳ.
  4. Loại giao dịch thỏa thuận khỏi mẫu số, theo (A4), để phiên thỏa thuận 14:45–15:00 không thổi phồng \(\text{DVOL}\) so với một tử số chỉ được đo trên phần khớp lệnh liên tục.
  5. Ưu tiên phiên bản song lũy thừa trong các ngày chạm biên độ. Biên độ giá theo ngày của Việt Nam khiến các bước dịch chuyển rời rạc lớn trở nên phổ biến, và Phương trình 14.15 là phiên bản trụ được trước chúng.
  6. Chỉ chuyển sang ước lượng vững với nhiễu khi buộc phải lấy mẫu dày. Các ước lượng trong Phương trình 14.13 là lựa chọn mặc định đúng ở năm phút. Nếu ứng dụng thực sự đòi hỏi lưới dày hơn, tài liệu đưa ra ba họ phương pháp, xếp theo chi phí triển khai tăng dần: ước lượng hai thang thời gian của Zhang, Mykland, và Aït-Sahalia (2005), lấy mẫu con ở hai tần suất và ngoại suy để loại bỏ nhiễu; cách tiếp cận tiền lấy trung bình (pre-averaging) của Jacod và c.s. (2009), lấy trung bình lợi nhuận trong từng khối trước khi bình phương; và các hạt nhân thực hiện (realized kernels) của Barndorff-Nielsen và c.s. (2008), gán trọng số cho các tự hiệp phương sai của lợi nhuận trong phiên. Hansen và Lunde (2006) là tài liệu tham chiếu chuẩn để chẩn đoán mức độ nhiễu hiện diện ngay từ đầu, và biểu đồ chữ ký (signature plot) của họ — \(\text{RV}\) theo tần suất lấy mẫu — nên được lập cho mặt cắt ngang Việt Nam trước khi thử bất kỳ điều nào ở trên.
  7. Cân nhắc cắt cụt theo láng giềng gần nhất như một phương án thay cho song lũy thừa. Andersen, Dobrev, và Schaumburg (2012) đề xuất các ước lượng MinRV và MedRV, vững với bước nhảy như \(\text{BV}\) nhưng hoạt động tốt hơn khi các bước nhảy đến ở các khoảng liền kề — một kịch bản thực tế vào ngày chạm biên độ, khi biên độ bị tiếp cận qua nhiều khoảng liên tiếp chứ không phải trong một lần dịch chuyển.
  8. Phần mềm. Không có gói đơn lẻ nào làm trọn vẹn việc này; \(\text{RV}\) và \(\text{BV}\) chỉ là vài dòng pandas, như trong phần mô phỏng ở trên. Các hàm realized trong hệ sinh thái arch bao phủ các ước lượng vững với nhiễu cho độc giả không muốn tự triển khai.

14.4.10 Những Sai lầm Thường gặp

  • Lấy mẫu quá dày. Đây là lỗi phổ biến nhất. Nhiều quan sát hơn trông có vẻ là nhiều thông tin hơn, nhưng Phương trình 14.18 giả định rằng thứ đang được lấy mẫu là giá hiệu quả. Quá vài phút trên HOSE thì điều đó không còn đúng nữa.
  • Coi lợi nhuận bằng không trong phiên là thông tin. Trên lưới năm phút, một mức giá cũ tạo ra lợi nhuận bằng không mà \(\text{RV}\) đọc thành độ biến động bằng không. Nếu không sàng lọc, điều này khiến những cổ phiếu kém thanh khoản nhất trông thanh khoản nhất — dấu của sai số ngược lại với phỏng đoán trực giác.
  • Nhầm lẫn thang đo. Bỏ đi hằng số \(\sqrt{2/\pi}\) sẽ làm Phương trình 14.14 lớn hơn Phương trình 37.2 khoảng 25%, và điều đó âm thầm phá vỡ mọi so sánh với các mức Amihud đã công bố trước đây hoặc với các phân loại ở những phần khác của chương.
  • Cho rằng mức cải thiện chuyển thành phần bù theo tỷ lệ một-đổi-một. Phương trình 14.21 nói rằng một biến phân loại sạch hơn sẽ nâng phần bù đo được tiến về phần bù thật. Nó không tạo ra một phần bù vốn không tồn tại, và nó cũng không khắc phục các vấn đề thiên kiến sống sót và hủy niêm yết vốn quyết định những quan sát công ty–tháng nào được đưa vào bảng dữ liệu ngay từ đầu.
  • Quên rằng mẫu số không thay đổi. Tất cả những điều trên chỉ cải thiện tử số. Khối lượng vẫn chịu ảnh hưởng của các quy ước báo cáo và các phức tạp liên quan đến giao dịch thỏa thuận đã thảo luận trong chương vi cấu trúc thị trường.

14.5 Lắp ráp Bảng dữ liệu Thanh khoản

Chúng ta ghép cả bảy thước đo vào một bảng dữ liệu công ty–tháng duy nhất để phân tích so sánh.

Merge all liquidity measures into a unified panel
# Start with monthly returns as the base
panel = monthly[['ticker', 'month_end', 'monthly_return',
                  'market_cap']].copy()

# Merge each liquidity measure
for name, df, key_col in [
    ('Amihud', amihud_monthly, 'amihud'),
    ('Zero Return', zero_monthly, 'zero_return_pct'),
    ('Turnover', turnover_monthly, 'log_turnover'),
    ('Roll', roll_monthly, 'roll_spread'),
    ('Corwin-Schultz', cs_monthly, 'cs_spread'),
    ('Quoted Spread', quoted_monthly, 'quoted_spread'),
    ('Kyle Lambda', kyle_monthly, 'log_kyle'),
]:
    panel = panel.merge(
        df[['ticker', 'month_end', key_col]],
        on=['ticker', 'month_end'],
        how='left'
    )

# Add log market cap
panel['log_mcap'] = np.log(panel['market_cap'].clip(lower=1))

# Add fundamentals (lagged)
fund_lagged = fundamentals.copy()
fund_lagged['merge_year'] = fund_lagged['fiscal_year'] + 1
panel = panel.merge(
    fund_lagged[['ticker', 'merge_year', 'book_equity']].rename(
        columns={'merge_year': 'year'}),
    left_on=['ticker', panel['month_end'].dt.year],
    right_on=['ticker', 'year'],
    how='left'
)
panel['bm'] = panel['book_equity'] / panel['market_cap']

print(f"Unified panel: {len(panel):,} firm-months")
print(f"\nCoverage by measure:")
liquidity_cols = ['amihud', 'zero_return_pct', 'log_turnover',
                   'roll_spread', 'cs_spread', 'quoted_spread', 'log_kyle']
for col in liquidity_cols:
    pct = panel[col].notna().mean()
    print(f"  {col:<20}: {pct:.1%}")

14.6 Đặc điểm Mặt cắt ngang của Thanh khoản

14.6.1 Thống kê Mô tả theo Nhóm Ngũ phân vị Quy mô

Thanh khoản biến thiên rất lớn trên phân phối quy mô. Các cổ phiếu vốn hóa nhỏ của Việt Nam có thể kém thanh khoản hơn các cổ phiếu vốn hóa lớn nhiều bậc độ lớn.

Compute liquidity statistics by market cap quintile
# Assign size quintiles within each month
panel['size_quintile'] = (
    panel.groupby('month_end')['market_cap']
    .transform(lambda x: pd.qcut(x, 5, labels=['Q1 (Small)', 'Q2',
                                                  'Q3', 'Q4',
                                                  'Q5 (Large)'],
                                   duplicates='drop'))
)

# Average liquidity by quintile
liq_by_size = (
    panel.groupby('size_quintile')[liquidity_cols]
    .mean()
    .round(4)
)

print("Average Liquidity by Market Cap Quintile:")
print(liq_by_size.to_string())
Bar charts of liquidity measures by size quintile
fig, axes = plt.subplots(2, 3, figsize=(16, 10))
axes = axes.flatten()

measures_to_plot = [
    ('amihud', 'Amihud (log)', '#2C5F8A'),
    ('zero_return_pct', 'Zero-Return %', '#C0392B'),
    ('log_turnover', 'Log Turnover', '#27AE60'),
    ('roll_spread', 'Roll Spread', '#E67E22'),
    ('cs_spread', 'Corwin-Schultz Spread', '#8E44AD'),
    ('quoted_spread', 'Quoted Spread', '#1ABC9C')
]

for i, (col, label, color) in enumerate(measures_to_plot):
    data = panel.groupby('size_quintile')[col].mean()
    axes[i].bar(range(len(data)), data.values,
                color=color, alpha=0.85, edgecolor='white')
    axes[i].set_xticks(range(len(data)))
    axes[i].set_xticklabels(data.index, fontsize=8)
    axes[i].set_ylabel(label)
    axes[i].set_title(label)

plt.suptitle('Liquidity Measures by Market Cap Quintile', fontsize=14)
plt.tight_layout()
plt.show()
Hình 14.1

14.6.2 Cấu trúc Tương quan

Các thước đo thanh khoản khác nhau tương quan mạnh đến mức nào? Nếu chúng nắm bắt cùng một chiều cơ bản, chúng ta kỳ vọng tương quan cao. Nếu chúng nắm bắt các chiều khác nhau (độ chặt so với độ sâu so với hoạt động giao dịch), tương quan sẽ ở mức vừa phải.

Compute and plot cross-sectional rank correlations
# Rank correlations (Spearman) among liquidity measures
# Reverse turnover sign so higher = less liquid (consistent direction)
panel_corr = panel[liquidity_cols].copy()
panel_corr['neg_log_turnover'] = -panel_corr['log_turnover']
corr_cols = ['amihud', 'zero_return_pct', 'neg_log_turnover',
              'roll_spread', 'cs_spread', 'quoted_spread', 'log_kyle']
corr_labels = ['Amihud', 'Zero-Return', 'Neg. Turnover', 'Roll',
                'Corwin-Schultz', 'Quoted Spread', 'Kyle λ']

rank_corr = panel_corr[corr_cols].corr(method='spearman')
rank_corr.index = corr_labels
rank_corr.columns = corr_labels

fig, ax = plt.subplots(figsize=(9, 8))
mask = np.triu(np.ones_like(rank_corr, dtype=bool), k=1)
sns.heatmap(
    rank_corr, mask=mask, annot=True, fmt='.2f',
    cmap='YlOrRd', vmin=0, vmax=1, square=True,
    linewidths=0.5, ax=ax,
    cbar_kws={'label': 'Spearman Rank Correlation'}
)
ax.set_title('Cross-Sectional Rank Correlations Among Liquidity Measures')
plt.tight_layout()
plt.show()
Hình 14.2

14.6.3 Phân tích Thành phần Chính của Thanh khoản

Do tính đa chiều của thanh khoản, chúng ta trích xuất một nhân tố thanh khoản tổng hợp bằng PCA:

Extract principal components from standardized liquidity measures
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

# Standardize each measure within each month (cross-sectional)
liq_data = panel[liquidity_cols].copy()
liq_data['neg_log_turnover'] = -liq_data['log_turnover']

pca_cols = ['amihud', 'zero_return_pct', 'neg_log_turnover',
             'roll_spread', 'cs_spread', 'log_kyle']

# Drop rows with any missing liquidity measure
liq_complete = panel.dropna(subset=pca_cols).copy()

# Cross-sectional standardization by month
def standardize_within_month(df, cols):
    for col in cols:
        df[col + '_z'] = (
            df.groupby('month_end')[col]
            .transform(lambda x: (x - x.mean()) / x.std())
        )
    return df

liq_complete = standardize_within_month(liq_complete, pca_cols)
z_cols = [c + '_z' for c in pca_cols]

# Pool all months for PCA
pca_input = liq_complete[z_cols].dropna()
pca = PCA(n_components=3)
pca.fit(pca_input)

print("PCA Explained Variance Ratios:")
for i, (var, cumvar) in enumerate(zip(
    pca.explained_variance_ratio_,
    np.cumsum(pca.explained_variance_ratio_)
)):
    print(f"  PC{i+1}: {var:.3f} (cumulative: {cumvar:.3f})")

print("\nPC1 Loadings:")
for col, loading in zip(pca_cols, pca.components_[0]):
    print(f"  {col:<20}: {loading:.3f}")

# Assign PC1 as composite illiquidity
liq_complete['illiq_pc1'] = pca.transform(
    liq_complete[z_cols].values
)[:, 0]

14.7 Thanh khoản Tổng hợp và Điều kiện Thị trường

14.7.1 Chuỗi Thời gian của Thanh khoản Thị trường

Thanh khoản tổng hợp (tức là mức phi thanh khoản trung bình trên tất cả cổ phiếu) biến đổi đáng kể theo thời gian. Chordia, Roll, và Subrahmanyam (2001) ghi nhận rằng thanh khoản toàn thị trường suy giảm trong các giai đoạn biến động cao và lợi nhuận thị trường âm.

Plot aggregate liquidity time series
# Compute monthly cross-sectional aggregates
agg_liquidity = (
    panel.groupby('month_end')
    .agg(
        amihud_median=('amihud', 'median'),
        zero_ret_median=('zero_return_pct', 'median'),
        turnover_median=('log_turnover', 'median'),
        roll_median=('roll_spread', 'median'),
        cs_median=('cs_spread', 'median'),
        n_stocks=('ticker', 'nunique')
    )
    .reset_index()
)

# Standardize for plotting
for col in ['amihud_median', 'zero_ret_median', 'roll_median']:
    agg_liquidity[col + '_z'] = (
        (agg_liquidity[col] - agg_liquidity[col].mean())
        / agg_liquidity[col].std()
    )

fig, axes = plt.subplots(2, 1, figsize=(14, 9), height_ratios=[2, 1])

# Panel A: Aggregate illiquidity
axes[0].plot(agg_liquidity['month_end'], agg_liquidity['amihud_median_z'],
             color='#2C5F8A', linewidth=1.5, label='Amihud')
axes[0].plot(agg_liquidity['month_end'], agg_liquidity['zero_ret_median_z'],
             color='#C0392B', linewidth=1.5, label='Zero-Return')
axes[0].plot(agg_liquidity['month_end'], agg_liquidity['roll_median_z'],
             color='#27AE60', linewidth=1.5, label='Roll Spread')
axes[0].axhline(y=0, color='gray', linewidth=0.5)
axes[0].set_ylabel('Standardized Illiquidity')
axes[0].set_title('Panel A: Aggregate Illiquidity Over Time')
axes[0].legend(fontsize=9)

# Shade crisis periods
crisis_periods = [
    ('2008-06-01', '2009-03-31', 'GFC'),
    ('2011-01-01', '2011-12-31', 'Tightening'),
    ('2020-02-01', '2020-05-31', 'COVID')
]
for start, end, label in crisis_periods:
    axes[0].axvspan(pd.Timestamp(start), pd.Timestamp(end),
                     alpha=0.15, color='gray')
    mid = pd.Timestamp(start) + (pd.Timestamp(end) - pd.Timestamp(start)) / 2
    axes[0].text(mid, axes[0].get_ylim()[1] * 0.9, label,
                 ha='center', fontsize=8, color='gray')

# Panel B: Market return
market_monthly = factors[['month_end', 'mkt_excess']].copy()
market_monthly['month_end'] = pd.to_datetime(market_monthly['month_end'])
axes[1].bar(market_monthly['month_end'],
            market_monthly['mkt_excess'] * 100,
            width=25,
            color=['#27AE60' if r > 0 else '#C0392B'
                   for r in market_monthly['mkt_excess']],
            alpha=0.6)
axes[1].set_ylabel('Market Excess Return (%)')
axes[1].set_xlabel('Date')
axes[1].set_title('Panel B: VN-Index Monthly Excess Return')

plt.tight_layout()
plt.show()
Hình 14.3

14.7.2 Tính Đồng biến của Thanh khoản

Chordia, Roll, và Subrahmanyam (2000) thấy rằng thanh khoản của từng cổ phiếu đồng biến với thanh khoản thị trường, ngay cả sau khi kiểm soát các nhân tố riêng của công ty. Chúng ta kiểm định tính đồng biến ở Việt Nam bằng cách hồi quy các thay đổi của thanh khoản cấp công ty trên các thay đổi của thanh khoản cấp thị trường:

\[ \Delta L_{i,m} = \alpha_i + \beta_i \Delta L_{M,m} + \gamma_i \Delta L_{M,m-1} + \delta_i \Delta L_{M,m+1} + \varepsilon_{i,m} \tag{14.22}\]

trong đó \(\Delta L_{i,m}\) là thay đổi mức phi thanh khoản của công ty \(i\), \(\Delta L_{M,m}\) là thay đổi mức phi thanh khoản trung bình thị trường (không tính công ty \(i\)), và các số hạng dẫn/trễ nắm bắt sự điều chỉnh không đồng bộ. Hệ số \(\beta_i\) đo độ nhạy của thanh khoản công ty \(i\) đối với các cú sốc thanh khoản toàn thị trường.

Test for commonality in liquidity across Vietnamese stocks
# Compute monthly changes in Amihud for each firm and the market
panel_common = panel[['ticker', 'month_end', 'amihud']].dropna().copy()
panel_common = panel_common.sort_values(['ticker', 'month_end'])
panel_common['d_amihud'] = (
    panel_common.groupby('ticker')['amihud'].diff()
)

# Market-level illiquidity change (equal-weighted, excluding firm i)
mkt_liq = (
    panel_common.groupby('month_end')['amihud']
    .mean()
    .diff()
    .to_frame('d_amihud_mkt')
)
mkt_liq['d_amihud_mkt_lag'] = mkt_liq['d_amihud_mkt'].shift(1)
mkt_liq['d_amihud_mkt_lead'] = mkt_liq['d_amihud_mkt'].shift(-1)

panel_common = panel_common.merge(mkt_liq, on='month_end', how='left')

# Estimate commonality for each firm
def estimate_commonality(group, min_obs=24):
    g = group.dropna(subset=['d_amihud', 'd_amihud_mkt'])
    if len(g) < min_obs:
        return None
    y = g['d_amihud']
    X = sm.add_constant(g[['d_amihud_mkt', 'd_amihud_mkt_lag',
                            'd_amihud_mkt_lead']])
    try:
        model = sm.OLS(y, X).fit()
        return pd.Series({
            'beta_mkt': model.params['d_amihud_mkt'],
            'beta_t': model.tvalues['d_amihud_mkt'],
            'r_squared': model.rsquared
        })
    except Exception:
        return None

commonality = (
    panel_common
    .groupby('ticker')
    .apply(estimate_commonality)
    .dropna()
)

print("Commonality in Liquidity (Amihud):")
print(f"  Mean beta_mkt: {commonality['beta_mkt'].mean():.3f}")
print(f"  Median beta_mkt: {commonality['beta_mkt'].median():.3f}")
print(f"  % significant at 5%: "
      f"{(commonality['beta_t'].abs() > 1.96).mean():.1%}")
print(f"  Mean R-squared: {commonality['r_squared'].mean():.3f}")

14.8 Thanh khoản có được Định giá không?

14.8.1 Phân loại Danh mục

Chúng ta kiểm định xem mức phi thanh khoản có dự báo lợi nhuận tương lai hay không bằng cách phân loại cổ phiếu vào các danh mục ngũ phân vị dựa trên các thước đo thanh khoản có độ trễ và so sánh lợi nhuận trung bình giữa các nhóm ngũ phân vị.

Sort stocks into quintiles by lagged illiquidity and compute returns
def liquidity_portfolio_sort(panel_df, liq_col, n_groups=5):
    """
    Compute quintile portfolio returns sorted on lagged liquidity.
    Lag the sorting variable by one month to avoid look-ahead bias.
    """
    df = panel_df[['ticker', 'month_end', 'monthly_return',
                    'market_cap', liq_col]].dropna().copy()
    df = df.sort_values(['ticker', 'month_end'])
    
    # Lag the sorting variable
    df['liq_lag'] = df.groupby('ticker')[liq_col].shift(1)
    df = df.dropna(subset=['liq_lag', 'monthly_return'])
    
    # Assign quintiles within each month
    df['quintile'] = (
        df.groupby('month_end')['liq_lag']
        .transform(lambda x: pd.qcut(x, n_groups, labels=False,
                                       duplicates='drop'))
    )
    
    # EW portfolio returns by quintile-month
    port_returns = (
        df.groupby(['month_end', 'quintile'])['monthly_return']
        .mean()
        .unstack()
    )
    
    # Long-short (Q5 - Q1)
    if n_groups - 1 in port_returns.columns and 0 in port_returns.columns:
        port_returns['long_short'] = (
            port_returns[n_groups - 1] - port_returns[0]
        )
    
    return port_returns

# Run sorts for each illiquidity measure
sort_measures = {
    'Amihud': 'amihud',
    'Zero-Return': 'zero_return_pct',
    'Neg. Turnover': 'log_turnover',  # Will reverse below
    'Roll Spread': 'roll_spread',
    'Corwin-Schultz': 'cs_spread',
}

# For turnover, negate so higher = less liquid
panel_sorts = panel.copy()
panel_sorts['neg_turnover'] = -panel_sorts['log_turnover']
sort_measures_actual = {
    'Amihud': 'amihud',
    'Zero-Return': 'zero_return_pct',
    'Neg. Turnover': 'neg_turnover',
    'Roll Spread': 'roll_spread',
    'Corwin-Schultz': 'cs_spread',
}

print("Liquidity Premium (EW, Quintile Sorts):")
print(f"{'Measure':<18} {'Q1 (Liquid)':>12} {'Q5 (Illiquid)':>14} "
      f"{'Q5-Q1':>10} {'t-stat':>8}")
print("-" * 62)

sort_results = {}
for name, col in sort_measures_actual.items():
    ports = liquidity_portfolio_sort(panel_sorts, col)
    sort_results[name] = ports
    
    q1 = ports[0].mean() * 12
    q5 = ports[4].mean() * 12 if 4 in ports.columns else np.nan
    ls = ports['long_short'].mean() * 12 if 'long_short' in ports else np.nan
    ls_se = ports['long_short'].std() / np.sqrt(len(ports)) * np.sqrt(12) if 'long_short' in ports else np.nan
    t = ls / ls_se if ls_se and ls_se > 0 else np.nan
    
    print(f"{name:<18} {q1:>12.4f} {q5:>14.4f} {ls:>10.4f} {t:>8.2f}")
Bar chart of quintile returns by liquidity measure
fig, axes = plt.subplots(2, 3, figsize=(16, 10))
axes = axes.flatten()

colors_quintile = ['#27AE60', '#2ECC71', '#F1C40F', '#E67E22', '#C0392B']

for i, (name, ports) in enumerate(sort_results.items()):
    if i >= 5:
        break
    quintile_means = [ports[q].mean() * 12 * 100 for q in range(5)
                       if q in ports.columns]
    axes[i].bar(range(len(quintile_means)), quintile_means,
                color=colors_quintile[:len(quintile_means)],
                alpha=0.85, edgecolor='white')
    axes[i].set_xticks(range(len(quintile_means)))
    axes[i].set_xticklabels([f'Q{q+1}' for q in range(len(quintile_means))])
    axes[i].set_ylabel('Annualized Return (%)')
    axes[i].set_title(name)
    axes[i].axhline(y=0, color='gray', linewidth=0.5)

# Hide unused subplot
if len(sort_results) < 6:
    axes[5].set_visible(False)

plt.suptitle('Average Returns by Illiquidity Quintile', fontsize=14)
plt.tight_layout()
plt.show()
Hình 14.4

14.8.2 Hồi quy Mặt cắt ngang Fama-MacBeth

Các phân loại danh mục mang tính thông tin nhưng không thể kiểm soát đồng thời nhiều đặc điểm. Chúng ta dùng các hồi quy mặt cắt ngang theo kiểu Fama và French (1993) để kiểm định xem thanh khoản có dự báo lợi nhuận sau khi kiểm soát quy mô, giá trị và động lượng hay không:

\[ R_{i,m+1} = \gamma_{0,m} + \gamma_{1,m} \text{ILLIQ}_{i,m} + \gamma_{2,m} \ln(\text{MCap}_{i,m}) + \gamma_{3,m} \text{BM}_{i,m} + \gamma_{4,m} R_{i,m-12:m-1} + \varepsilon_{i,m+1} \tag{14.23}\]

Trung bình chuỗi thời gian của hệ số theo tháng \(\bar{\gamma}_1\) ước lượng phần bù phi thanh khoản, và thống kê t của nó dùng sai số chuẩn của Fama và French (1993).

Fama-MacBeth regressions of returns on lagged liquidity and controls
def fama_macbeth(panel_df, illiq_col, controls=['log_mcap', 'bm'],
                  min_stocks=50):
    """
    Run Fama-MacBeth cross-sectional regressions of
    next-month returns on lagged illiquidity and controls.
    """
    df = panel_df.copy()
    df = df.sort_values(['ticker', 'month_end'])
    
    # Lag the illiquidity measure
    df['illiq_lag'] = df.groupby('ticker')[illiq_col].shift(1)
    
    # Lag controls
    for c in controls:
        df[c + '_lag'] = df.groupby('ticker')[c].shift(1)
    
    regressors = ['illiq_lag'] + [c + '_lag' for c in controls]
    df = df.dropna(subset=['monthly_return'] + regressors)
    
    # Month-by-month cross-sectional regressions
    months = sorted(df['month_end'].unique())
    gamma_list = []
    
    for month in months:
        cross = df[df['month_end'] == month]
        if len(cross) < min_stocks:
            continue
        
        y = cross['monthly_return'].values
        X = sm.add_constant(cross[regressors].values)
        
        try:
            model = sm.OLS(y, X).fit()
            gammas = {'month_end': month, 'intercept': model.params[0]}
            for j, reg in enumerate(regressors):
                gammas[reg] = model.params[j + 1]
            gamma_list.append(gammas)
        except Exception:
            pass
    
    gamma_df = pd.DataFrame(gamma_list)
    
    # Time-series averages and t-statistics
    results = {}
    for col in ['intercept'] + regressors:
        mean = gamma_df[col].mean()
        se = gamma_df[col].std() / np.sqrt(len(gamma_df))
        t = mean / se if se > 0 else np.nan
        results[col] = {'Coefficient': mean, 'SE': se, 't-stat': t}
    
    return pd.DataFrame(results).T, gamma_df

# Run for each illiquidity measure
print("Fama-MacBeth Regressions: R_{i,m+1} on ILLIQ_{i,m} + controls")
print("=" * 70)

for name, col in [('Amihud', 'amihud'),
                    ('Zero-Return', 'zero_return_pct'),
                    ('Roll Spread', 'roll_spread'),
                    ('Corwin-Schultz', 'cs_spread')]:
    results, gammas = fama_macbeth(panel, col)
    print(f"\n{name}:")
    print(results[['Coefficient', 't-stat']].round(4).to_string())

14.8.3 Phần bù Thanh khoản đã Điều chỉnh Nhân tố

Phần bù thanh khoản có thể được giải thích một phần hoặc toàn bộ bởi các nhân tố rủi ro hiện có (quy mô, giá trị, động lượng). Chúng ta kiểm định điều này bằng cách hồi quy lợi nhuận danh mục mua–bán khống thanh khoản trên các nhân tố Fama-French-Carhart:

Regress liquidity long-short returns on FF+Carhart factors
print("Factor-Adjusted Liquidity Premium:")
print(f"{'Measure':<18} {'Alpha (ann.)':>12} {'Alpha t':>10} "
      f"{'MKT':>8} {'SMB':>8} {'HML':>8} {'R2':>6}")
print("-" * 72)

for name, ports in sort_results.items():
    if 'long_short' not in ports.columns:
        continue
    
    ls_series = ports['long_short'].to_frame('ls')
    ls_series.index = pd.to_datetime(ls_series.index)
    
    merged = ls_series.merge(factors, left_index=True,
                              right_on='month_end', how='inner')
    
    if len(merged) < 24:
        continue
    
    y = merged['ls']
    X = sm.add_constant(merged[['mkt_excess', 'smb', 'hml', 'wml']])
    
    model = sm.OLS(y, X).fit(cov_type='HAC', cov_kwds={'maxlags': 6})
    
    alpha_ann = model.params['const'] * 12
    alpha_t = model.tvalues['const']
    mkt_b = model.params['mkt_excess']
    smb_b = model.params['smb']
    hml_b = model.params['hml']
    r2 = model.rsquared
    
    print(f"{name:<18} {alpha_ann:>12.4f} {alpha_t:>10.2f} "
          f"{mkt_b:>8.3f} {smb_b:>8.3f} {hml_b:>8.3f} {r2:>6.3f}")

14.9 Thanh khoản và Ước lượng Chi phí Giao dịch

14.9.1 Chuyển các Thước đo thành Chi phí Giao dịch

Đối với các nhà thực hành, câu hỏi then chốt là: một giá trị Amihud hay chênh lệch giá nhất định có ý nghĩa gì về chi phí VND thực tế cho mỗi giao dịch? Chúng ta hiệu chỉnh mối quan hệ giữa các thước đo xấp xỉ tần suất thấp và chi phí giao dịch tường minh.

Calibrate liquidity proxies to estimated round-trip trading costs
def estimate_round_trip_cost(row):
    """
    Estimate total round-trip trading cost (in %) from
    multiple liquidity proxies.
    
    Components:
    1. Explicit: commission + tax (~0.35% round-trip)
    2. Spread cost: half-spread each way
    3. Price impact: function of trade size
    """
    explicit = 0.0035  # 35 bps round-trip
    
    # Use Corwin-Schultz or quoted spread as spread estimate
    spread = row.get('cs_spread', row.get('quoted_spread', 0.005))
    spread_cost = spread  # Full spread = round-trip cost
    
    # Price impact (approximate from Amihud)
    # For a trade of 1% of daily volume
    amihud_raw = row.get('amihud_raw', 0)
    impact = amihud_raw * 0.01  # Rough approximation
    
    return explicit + spread_cost + impact

panel['estimated_rtc'] = panel.apply(estimate_round_trip_cost, axis=1)

# Distribution by size quintile
rtc_by_size = (
    panel.groupby('size_quintile')['estimated_rtc']
    .agg(['mean', 'median', 'std'])
    .round(4)
)
print("Estimated Round-Trip Cost by Size Quintile (%):")
print((rtc_by_size * 100).round(2).to_string())
Plot estimated trading costs by size quintile
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Panel A: Distribution
for q, color in zip(['Q1 (Small)', 'Q3', 'Q5 (Large)'],
                     ['#C0392B', '#F1C40F', '#27AE60']):
    subset = panel[panel['size_quintile'] == q]['estimated_rtc'].dropna()
    subset = subset[subset < 0.15]  # Trim extreme
    axes[0].hist(subset * 100, bins=50, density=True, alpha=0.5,
                  color=color, label=q, edgecolor='white')
axes[0].set_xlabel('Estimated Round-Trip Cost (%)')
axes[0].set_ylabel('Density')
axes[0].set_title('Panel A: Cost Distribution by Size')
axes[0].legend()

# Panel B: Time series of median cost
cost_ts = (
    panel.groupby('month_end')['estimated_rtc']
    .median()
    .reset_index()
)
axes[1].plot(pd.to_datetime(cost_ts['month_end']),
             cost_ts['estimated_rtc'] * 100,
             color='#2C5F8A', linewidth=1.5)
axes[1].set_xlabel('Date')
axes[1].set_ylabel('Median Round-Trip Cost (%)')
axes[1].set_title('Panel B: Aggregate Trading Costs Over Time')

plt.tight_layout()
plt.show()
Hình 14.5

14.9.2 Tính Khả thi của Chiến lược

Một ứng dụng quan trọng của việc đo lường thanh khoản là kiểm định xem một chiến lược khai thác hiện tượng bất thường có còn sinh lời sau khi tính đến chi phí giao dịch thực tế hay không. Chúng ta tính lợi nhuận sau chi phí cho chính các danh mục phân loại theo thanh khoản. Đây vốn là một phép kiểm định thận trọng vì vị thế mua kém thanh khoản chịu chi phí cao nhất.

Compute net-of-cost returns for illiquidity-sorted portfolios
# For each quintile, estimate average monthly turnover and cost
# and subtract from gross returns
for name, ports in sort_results.items():
    if 'long_short' not in ports.columns:
        continue
    
    gross_ann = ports['long_short'].mean() * 12
    
    # Estimate costs: illiquid quintile has higher costs
    # Assume monthly turnover of ~15% for long-short with monthly rebalancing
    turnover = 0.15
    cost_q1 = 0.003  # 30 bps per trade for liquid stocks
    cost_q5 = 0.015  # 150 bps for illiquid stocks
    avg_cost = (cost_q1 + cost_q5) / 2  # Average across long and short
    monthly_tc = turnover * avg_cost
    
    net_ann = gross_ann - monthly_tc * 12
    
    print(f"{name:<18}: Gross = {gross_ann*100:>6.2f}%, "
          f"TC = {monthly_tc*1200:>6.1f} bps/mo, "
          f"Net = {net_ann*100:>6.2f}%")

14.10 Thanh khoản trong Giai đoạn Căng thẳng Thị trường

14.10.1 Tháo chạy sang Thanh khoản

Trong giai đoạn căng thẳng thị trường, nhà đầu tư bán các tài sản kém thanh khoản và mua các tài sản thanh khoản, đó là hiện tượng “tháo chạy sang thanh khoản” (flight to liquidity) làm nới rộng chênh lệch lợi nhuận giữa các cổ phiếu thanh khoản và kém thanh khoản. Hameed, Kang, và Viswanathan (2010) chỉ ra rằng dạng thức này mạnh nhất khi lợi nhuận thị trường âm nhất.

Test for flight-to-liquidity during market stress
# Merge Amihud-sorted portfolio returns with market returns
amihud_ports = sort_results.get('Amihud')
if amihud_ports is not None and 'long_short' in amihud_ports.columns:
    ftl_data = pd.merge(
        amihud_ports['long_short'].to_frame('illiq_premium'),
        factors[['month_end', 'mkt_excess']].set_index('month_end'),
        left_index=True, right_index=True, how='inner'
    )
    
    # Classify market states
    ftl_data['mkt_state'] = pd.cut(
        ftl_data['mkt_excess'],
        bins=[-np.inf,
              ftl_data['mkt_excess'].quantile(0.20),
              ftl_data['mkt_excess'].quantile(0.80),
              np.inf],
        labels=['Bear (bottom 20%)', 'Normal', 'Bull (top 20%)']
    )
    
    # Illiquidity premium by market state
    state_premium = (
        ftl_data.groupby('mkt_state')['illiq_premium']
        .agg(['mean', 'std', 'count'])
    )
    state_premium['ann_premium'] = state_premium['mean'] * 12
    state_premium['t_stat'] = (
        state_premium['mean']
        / (state_premium['std'] / np.sqrt(state_premium['count']))
    )
    
    print("Illiquidity Premium by Market State:")
    print(state_premium[['ann_premium', 't_stat', 'count']].round(3))
Bar chart of conditional illiquidity premium
fig, ax = plt.subplots(figsize=(8, 5))

if 'state_premium' in dir():
    colors_state = ['#C0392B', '#F1C40F', '#27AE60']
    bars = ax.bar(range(len(state_premium)),
                   state_premium['ann_premium'] * 100,
                   color=colors_state, alpha=0.85, edgecolor='white')
    ax.set_xticks(range(len(state_premium)))
    ax.set_xticklabels(state_premium.index)
    ax.set_ylabel('Annualized Q5-Q1 Return (%)')
    ax.set_title('Illiquidity Premium by Market State')
    ax.axhline(y=0, color='gray', linewidth=0.8)
    
    for i, (_, row) in enumerate(state_premium.iterrows()):
        ax.text(i, row['ann_premium'] * 100 + 0.3,
                f"t={row['t_stat']:.1f}",
                ha='center', fontsize=10)

plt.tight_layout()
plt.show()
Hình 14.6

14.10.2 Đồng biến của Thanh khoản với Rủi ro Toàn cầu

Thanh khoản thị trường Việt Nam có thể bị chi phối bởi các nhân tố rủi ro toàn cầu, đặc biệt đối với các cổ phiếu do nhà đầu tư nước ngoài nắm giữ. Chúng ta kiểm định xem các thước đo rủi ro toàn cầu (VIX, sức mạnh của USD) có dự báo thanh khoản tổng hợp của Việt Nam hay không:

Regress Vietnamese aggregate liquidity on global risk proxies
# Merge aggregate liquidity with global variables
global_vars = client.get_macro_data(
    variables=['vix_close', 'dxy_index'],
    start_date='2008-01-01',
    end_date='2024-12-31',
    frequency='monthly'
)

agg_liq_global = agg_liquidity.merge(
    global_vars, on='month_end', how='inner'
)

# Changes in all variables
for col in ['amihud_median', 'vix_close', 'dxy_index']:
    agg_liq_global[f'd_{col}'] = agg_liq_global[col].diff()

# Regression
y = agg_liq_global['d_amihud_median'].dropna()
X = sm.add_constant(
    agg_liq_global.loc[y.index, ['d_vix_close', 'd_dxy_index']]
)

model = sm.OLS(y, X).fit(cov_type='HAC', cov_kwds={'maxlags': 3})
print("Aggregate Illiquidity ~ Global Risk:")
print(model.summary().tables[1])

14.11 Xây dựng một Nhân tố Thanh khoản có thể Giao dịch

Theo Pástor và Stambaugh (2003), chúng ta xây dựng một nhân tố thanh khoản tổng hợp có thể dùng trong các kiểm định định giá tài sản. Nhân tố này nắm bắt các cú sốc (innovations) của thanh khoản tổng hợp (những thay đổi không được kỳ vọng của điều kiện giao dịch toàn thị trường).

Construct an aggregate liquidity innovation factor
# Step 1: Compute market-level Amihud as EW average
mkt_amihud = (
    panel.groupby('month_end')['amihud']
    .mean()
    .to_frame('mkt_amihud')
)

# Step 2: Estimate AR(2) model for aggregate illiquidity
mkt_amihud['mkt_amihud_lag1'] = mkt_amihud['mkt_amihud'].shift(1)
mkt_amihud['mkt_amihud_lag2'] = mkt_amihud['mkt_amihud'].shift(2)
mkt_amihud = mkt_amihud.dropna()

ar_model = sm.OLS(
    mkt_amihud['mkt_amihud'],
    sm.add_constant(mkt_amihud[['mkt_amihud_lag1', 'mkt_amihud_lag2']])
).fit()

# Step 3: Residuals = liquidity innovations
# Negative innovation = liquidity improved (good)
# Positive innovation = liquidity deteriorated (bad)
mkt_amihud['liq_innovation'] = ar_model.resid

# Step 4: Test whether liquidity innovations predict returns
# Higher sensitivity to negative innovations = higher expected return
print("AR(2) Model for Aggregate Amihud:")
print(f"  R-squared: {ar_model.rsquared:.3f}")
print(f"  AR(1) coef: {ar_model.params['mkt_amihud_lag1']:.3f} "
      f"(t={ar_model.tvalues['mkt_amihud_lag1']:.2f})")

# Step 5: Estimate liquidity betas for each firm
panel_liq_beta = panel.merge(
    mkt_amihud[['liq_innovation']],
    left_on='month_end', right_index=True, how='inner'
)

def estimate_liq_beta(group, min_obs=36):
    g = group.dropna(subset=['monthly_return', 'liq_innovation'])
    if len(g) < min_obs:
        return None
    y = g['monthly_return']
    X = sm.add_constant(g['liq_innovation'])
    try:
        model = sm.OLS(y, X).fit()
        return model.params['liq_innovation']
    except Exception:
        return None

liq_betas = (
    panel_liq_beta
    .groupby('ticker')
    .apply(estimate_liq_beta)
    .dropna()
    .to_frame('liq_beta')
)

print(f"\nLiquidity Beta Distribution:")
print(liq_betas['liq_beta'].describe().round(4))

14.12 Hướng dẫn Thực hành cho Việt Nam

Phân tích trong chương này đưa ra các khuyến nghị sau:

Dành cho nhà nghiên cứu: Tỷ số phi thanh khoản Amihud là thước đo xấp xỉ thanh khoản đa dụng tốt nhất cho chứng khoán Việt Nam. Nó có độ bao phủ cao nhất, khả năng dự báo lợi nhuận mặt cắt ngang mạnh nhất, và mối quan hệ vững chắc nhất với quy mô công ty. Khi cần một thước đo thứ hai để kiểm tra độ vững, tỷ lệ lợi nhuận bằng không là bổ sung tự nhiên—nó nắm bắt một chiều khác (ngưỡng chi phí giao dịch) và có độ bao phủ gần như đầy đủ.

Dành cho xây dựng danh mục đầu tư: Mọi kiểm định ngược của một chiến lược chứng khoán Việt Nam nên tính và báo cáo chi phí khứ hồi ước lượng theo nhóm ngũ phân vị. Các chiến lược tập trung vào hai nhóm ngũ phân vị quy mô thấp nhất chịu chi phí 2–5% mỗi vòng khứ hồi, khiến việc tái cân bằng hằng tháng không kinh tế. Tái cân bằng theo quý hoặc theo năm với ràng buộc về tỷ lệ vòng quay thì thực tế hơn.

Dành cho quản trị rủi ro: Theo dõi điều kiện thanh khoản tổng hợp bằng trung vị mặt cắt ngang của Amihud hoặc tỷ lệ lợi nhuận bằng không toàn thị trường. Sự suy giảm thanh khoản dự báo lợi nhuận thị trường âm và chênh lệch giá rộng hơn trong các tháng tiếp theo. Hãy siết chặt giới hạn rủi ro khi mức phi thanh khoản tổng hợp vượt phân vị 90 lịch sử.

Dành cho so sánh quốc tế: Khi so sánh các phần bù nhân tố của Việt Nam với bằng chứng của Mỹ hoặc các thị trường phát triển khác, hãy luôn báo cáo kết quả trên một tập con “nhóm cổ phiếu thanh khoản” (60% hàng đầu theo vốn hóa thị trường) bên cạnh toàn bộ mẫu. Nhiều hiện tượng bất thường trông lớn trong toàn bộ mẫu thu hẹp đáng kể khi giới hạn ở các cổ phiếu thực sự có thể giao dịch ở quy mô lớn.

14.13 Tóm tắt

Bảng Bảng 14.3 trình bày các thước đo thanh khoản khác nhau.

Bảng 14.3: Tóm tắt các thuộc tính của các thước đo thanh khoản trên thị trường Việt Nam.
Thước đo Chiều Độ bao phủ Gradient theo Quy mô Khả năng Dự báo Lợi nhuận Cách dùng Khuyến nghị
Amihud Tác động giá Cao Rất dốc Mạnh Thước đo chính; phân loại danh mục; Fama-MacBeth
Lợi nhuận bằng không Chi phí giao dịch Đầy đủ Dốc Mạnh Kiểm tra độ vững; nghiên cứu thị trường mới nổi
Tỷ lệ vòng quay Hoạt động giao dịch Cao Vừa phải Vừa phải Bộ lọc dựa trên khối lượng; phân tích dòng tiền
Chênh lệch Roll Độ chặt Vừa phải Vừa phải Vừa phải Ước lượng chênh lệch giá khi không có dữ liệu mua–bán
Corwin-Schultz Độ chặt Vừa phải Vừa phải Vừa phải Chênh lệch giá dựa trên cao–thấp; hiệu chỉnh
Chênh lệch giá chào Độ chặt Thay đổi Dốc Mạnh Thước đo trực tiếp khi có sẵn
Lambda của Kyle Tác động giá Vừa phải Dốc Mạnh Nghiên cứu vi cấu trúc thị trường

Thanh khoản không phải là một cân nhắc thứ yếu đối với nghiên cứu chứng khoán Việt Nam; nó là yếu tố quyết định bậc nhất việc chiến lược nào có thể triển khai, hiện tượng bất thường nào là thật, và kết quả nào là sản phẩm giả tạo của việc giao dịch những cổ phiếu thực tế không thể giao dịch. Mọi phát hiện thực nghiệm trong cuốn sách này nên được đánh giá qua lăng kính của phân tích thanh khoản được phát triển trong chương này.