30  Chất lượng và thời điểm công bố thông tin

Công bố thông tin doanh nghiệp là cơ chế chính để các công ty giao tiếp với thị trường vốn. Chất lượng, khối lượng và thời điểm công bố thông tin định hình môi trường thông tin, trong đó nhà đầu tư hình thành kỳ vọng, định giá chứng khoán và phân bổ vốn. Một nền văn liệu lý thuyết và thực nghiệm đồ sộ, được tổng quan bởi Healy và Palepu (2001) và Beyer và c.s. (2010), cho thấy các quyết định công bố thông tin có tác động bậc nhất lên chi phí vốn, thanh khoản và hiệu quả đầu tư.

Chương này đưa hai thập kỷ nghiên cứu về công bố thông tin vào thị trường Việt Nam, nơi một số đặc điểm thể chế tạo nên bối cảnh đặc thù. Thứ nhất, khung pháp lý của Việt Nam, dựa trên Thông tư 155/2015/TT-BTC (được sửa đổi bởi Thông tư 96/2020/TT-BTC) và được Ủy ban Chứng khoán Nhà nước (SSC) giám sát thực thi, quy định các nghĩa vụ công bố định kỳ và theo sự kiện với thời hạn cụ thể khác với chuẩn mực của Hoa Kỳ và châu Âu. Thứ hai, sự thống trị của nhà đầu tư cá nhân và mức độ phủ sóng tương đối mỏng của các nhà phân tích khiến thông tin do doanh nghiệp công bố thường là nguồn thông tin chủ yếu về riêng từng doanh nghiệp, làm tăng tầm quan trọng kinh tế của chúng. Thứ ba, quá trình chuyển đổi đang diễn ra từ Chuẩn mực Kế toán Việt Nam (VAS) sang hội tụ với IFRS đưa vào những thay đổi theo thời gian đối với yêu cầu công bố thông tin, tạo ra biến thiên tự nhiên cho phân tích thực nghiệm.

30.1 Nền tảng lý thuyết

30.1.1 Lý thuyết công bố thông tin tự nguyện

Mô hình nền tảng về công bố thông tin tự nguyện thuộc về Verrecchia (1983), người chỉ ra rằng khi nhà đầu tư biết nhà quản lý nắm giữ thông tin riêng, một trạng thái cân bằng bóc tách (unraveling) xuất hiện: sự im lặng bị diễn giải là tin xấu, nên nhà quản lý sẽ công bố trừ khi chi phí độc quyền thông tin (proprietary cost) của việc công bố vượt quá lợi ích. Điểm mấu chốt là việc không công bố cũng mang thông tin, vì nhà đầu tư suy luận một cách hợp lý rằng thông tin bị giữ lại là bất lợi.

Diamond (1985) mở rộng phân tích sang bối cảnh nhiều thời kỳ, trong đó chính sách công bố thông tin của doanh nghiệp ảnh hưởng đến độ chính xác của thông tin công khai và do đó ảnh hưởng đến động cơ thu thập thông tin riêng. Sự đánh đổi trung tâm là giữa việc giảm bất cân xứng thông tin (làm giảm chi phí vốn) và việc giảm các khoản đặc lợi mà nhà giao dịch có thông tin kiếm được (có thể làm nản lòng hoạt động giám sát). Diamond và Verrecchia (1991) chính thức hóa mối liên hệ giữa công bố thông tin và thanh khoản: bằng cách giảm lựa chọn đối nghịch, công bố thông tin tự nguyện thu hẹp chênh lệch giá mua–bán và làm tăng mức sẵn lòng giao dịch của nhà đầu tư không có thông tin.

Dự báo thực nghiệm là các doanh nghiệp có chất lượng công bố thông tin cao hơn sẽ được hưởng:

  1. Chi phí vốn cổ phần thấp hơn (Botosan 1997; Botosan và Plumlee 2002)
  2. Chi phí nợ thấp hơn (Sengupta 1998)
  3. Thanh khoản cao hơn và chênh lệch giá mua–bán thấp hơn (Diamond và Verrecchia 1991; Lang, Lins, và Maffett 2012)
  4. Các quyết định đầu tư hiệu quả hơn (Biddle, Hilary, và Verdi 2009)

30.1.2 Thời điểm công bố thông tin mang tính chiến lược

Không phải mọi thông tin công bố đều tự nguyện về thời điểm, nhưng nhà quản lý vẫn giữ quyền tùy nghi về việc khi nào công bố trong các khung thời gian cho phép. Patell và Wolfson (1982) ghi nhận rằng các doanh nghiệp có xu hướng công bố tin tốt trong giờ giao dịch và tin xấu sau khi thị trường đóng cửa. DellaVigna và Pollet (2009) chỉ ra rằng lợi nhuận được công bố vào thứ Sáu (tức là khi sự chú ý của nhà đầu tư thấp hơn) tạo ra phản ứng tức thời nhỏ hơn và hiện tượng trôi dạt sau thông báo lớn hơn, phù hợp với giả thuyết chú ý hạn chế. Hirshleifer, Lim, và Teoh (2009) khái quát hóa phát hiện này: các sự kiện không liên quan làm phân tán nhà đầu tư (chẳng hạn một số lượng lớn thông báo diễn ra đồng thời) làm giảm phản ứng giá tức thời đối với tin tức lợi nhuận.

Ở Việt Nam, một số đặc điểm khiến việc chọn thời điểm chiến lược đặc biệt đáng chú ý. Lịch công bố thông tin tập trung, khi nhiều doanh nghiệp nộp hồ sơ sát hạn quy định, tạo ra biến thiên tự nhiên về mức độ ùn tắc thông báo. Cơ sở nhà đầu tư do cá nhân chi phối có thể dễ chịu tác động của hiệu ứng chú ý hơn nhà đầu tư tổ chức. Cấu trúc quy định, vốn áp dụng chế tài đối với việc nộp muộn nhưng cho phép tùy nghi trong khung thời gian cho phép, tạo ra bối cảnh trong đó việc lựa chọn ngày nộp hồ sơ tự nó mang thông tin.

30.1.3 Chất lượng công bố thông tin tại các thị trường mới nổi

Ball, Robin, và Wu (2003) lập luận rằng chất lượng kế toán được định hình bởi động cơ lập báo cáo nhiều hơn là bởi chuẩn mực kế toán. Trong các môi trường thể chế có thực thi yếu, sở hữu tập trung và báo cáo tài chính gắn chặt với báo cáo thuế, doanh nghiệp có thể tạo ra thông tin công bố chất lượng thấp hơn ngay cả khi chuẩn mực danh nghĩa là nghiêm ngặt. Leuz, Nanda, và Wysocki (2003) xác nhận mô hình này trên phạm vi quốc tế: quản trị lợi nhuận (một thước đo thay thế đảo ngược của chất lượng công bố thông tin) cao nhất ở các quốc gia có bảo vệ nhà đầu tư yếu, sở hữu tập trung và thị trường vốn kém phát triển.

Việt Nam thể hiện một số đặc điểm này. Bushman và c.s. (2004) phân loại các yếu tố quyết định tính minh bạch thành các yếu tố quản trị (nguồn gốc hệ thống pháp luật, hiệu quả tư pháp, bảo vệ cổ đông thiểu số) và các yếu tố chính trị (sở hữu nhà nước, sự can thiệp của chính phủ). Truyền thống luật thành văn của Việt Nam, sở hữu nhà nước đáng kể trong các doanh nghiệp niêm yết và năng lực thực thi đang phát triển cho thấy chất lượng công bố thông tin về trung bình có thể thấp hơn so với các thị trường phát triển, nhưng có biến thiên đáng kể giữa các doanh nghiệp do cấu trúc quản trị và sở hữu ở cấp doanh nghiệp.

30.2 Khung pháp lý

30.2.1 Các yêu cầu công bố thông tin bắt buộc

Quy định về công bố thông tin của Việt Nam vận hành thông qua một hệ thống thứ bậc các văn bản pháp lý:

  • Luật Chứng khoán (2019): Thiết lập nghĩa vụ chung của các doanh nghiệp niêm yết là công bố thông tin trung thực, chính xác, đầy đủ và kịp thời (Điều 118).
  • Thông tư 155/2015/TT-BTC (được sửa đổi bởi Thông tư 96/2020/TT-BTC): Quy định nội dung, định dạng và thời hạn đối với các lần công bố định kỳ và theo sự kiện.
  • Các quyết định và hướng dẫn của SSC: Cung cấp chi tiết triển khai và các yêu cầu theo từng ngành.

Các thời hạn báo cáo định kỳ chính được nêu trong Bảng 30.1

Bảng 30.1: Thời hạn công bố thông tin định kỳ theo quy định chứng khoán của Việt Nam.
Loại báo cáo Thời hạn Yêu cầu kiểm toán
Báo cáo tài chính năm 90 ngày sau khi kết thúc năm tài chính Được kiểm toán
Báo cáo tài chính bán niên 45 ngày sau khi kết thúc kỳ Được soát xét
Báo cáo tài chính quý 20 ngày sau khi kết thúc quý Chưa kiểm toán
Báo cáo thường niên 110 ngày sau khi kết thúc năm tài chính Không áp dụng (tường thuật)

Thông tin công bố theo sự kiện (bất thường) phải được nộp trong vòng 24 giờ đối với các sự kiện trọng yếu, bao gồm thay đổi sở hữu vượt quá 1% của các cổ đông lớn, nghị quyết của hội đồng quản trị về cổ tức hoặc tăng vốn, và bất kỳ sự kiện nào có thể ảnh hưởng đáng kể đến giá cổ phiếu.

30.2.2 Chế tài đối với hành vi không tuân thủ

SSC có thể áp dụng phạt hành chính đối với việc công bố thông tin muộn hoặc không đầy đủ, thường từ 50–100 triệu VND đối với vi phạm nhẹ và lên đến 500 triệu VND đối với các thiếu sót trọng yếu. Mặc dù các khoản tiền này khá nhỏ so với quy mô của các công ty vốn hóa lớn, chi phí danh tiếng và rủi ro bị đình chỉ giao dịch tạo thêm sức răn đe.

30.3 Xây dựng dữ liệu

30.3.1 Nạp các thư viện cần thiết

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
import statsmodels.formula.api as smf
from datetime import datetime, timedelta
from scipy import stats
from sklearn.preprocessing import StandardScaler
from linearmodels.panel import PanelOLS
import warnings
warnings.filterwarnings('ignore')

# Plotting defaults
plt.rcParams.update({
    'figure.figsize': (10, 6),
    'figure.dpi': 150,
    'font.size': 11,
    'axes.spines.top': False,
    'axes.spines.right': False
})

30.3.2 Truy xuất dữ liệu công bố thông tin

Chúng ta giả định rằng mình có dữ liệu có cấu trúc về ngày nộp hồ sơ, dấu thời gian thông báo và nội dung văn bản của các thông tin công bố của doanh nghiệp đối với tất cả các công ty.

Load disclosure and financial data
from datacore import DataCoreClient

client = DataCoreClient()

# Filing metadata: announcement dates, filing dates, report types
filings = client.get_filings(
    exchanges=['HOSE', 'HNX'],
    report_types=['annual', 'semi_annual', 'quarterly'],
    start_date='2012-01-01',
    end_date='2024-12-31',
    fields=[
        'ticker', 'report_type', 'fiscal_year', 'fiscal_quarter',
        'fiscal_year_end', 'filing_date', 'announcement_date',
        'auditor', 'audit_opinion', 'file_url'
    ]
)

# Financial statement data
financials = client.get_fundamentals(
    exchanges=['HOSE', 'HNX'],
    start_date='2012-01-01',
    end_date='2024-12-31',
    fields=[
        'ticker', 'fiscal_year', 'fiscal_quarter',
        'total_assets', 'total_equity', 'revenue', 'net_income',
        'operating_cash_flow', 'total_accruals',
        'market_cap', 'book_to_market'
    ]
)

# Daily trading data for event studies
trading = client.get_daily_prices(
    exchanges=['HOSE', 'HNX'],
    start_date='2012-01-01',
    end_date='2024-12-31',
    fields=[
        'ticker', 'date', 'close', 'volume', 'turnover',
        'bid_ask_spread', 'market_return'
    ]
)

# Ownership and governance
governance = client.get_governance(
    exchanges=['HOSE', 'HNX'],
    fields=[
        'ticker', 'fiscal_year', 'state_ownership_pct',
        'foreign_ownership_pct', 'board_size',
        'board_independence_pct', 'big4_auditor',
        'dual_listing'
    ]
)

print(f"Filings: {filings.shape[0]:,} records")
print(f"Financials: {financials.shape[0]:,} records")
print(f"Trading: {trading.shape[0]:,} records")
print(f"Governance: {governance.shape[0]:,} records")

30.3.3 Tính độ kịp thời của việc nộp hồ sơ

Chúng ta định nghĩa độ trễ báo cáo là số ngày theo lịch giữa ngày kết thúc kỳ tài chính và ngày báo cáo tài chính của doanh nghiệp được công bố công khai. Đối với báo cáo năm, mức tối đa theo quy định là 90 ngày; các doanh nghiệp nộp sớm hơn thời hạn sẽ tiết lộ thông tin sớm hơn, trong khi các doanh nghiệp nộp muộn đối mặt với khả năng bị phạt và phát đi tín hiệu về những khó khăn có thể có trong sổ sách của mình.

Compute reporting lag for each filing
filings['fiscal_year_end'] = pd.to_datetime(filings['fiscal_year_end'])
filings['filing_date'] = pd.to_datetime(filings['filing_date'])
filings['announcement_date'] = pd.to_datetime(filings['announcement_date'])

# Reporting lag = filing date - fiscal period end
filings['reporting_lag'] = (
    filings['filing_date'] - filings['fiscal_year_end']
).dt.days

# Regulatory deadline based on report type
deadline_map = {
    'annual': 90,
    'semi_annual': 45,
    'quarterly': 20
}
filings['deadline_days'] = filings['report_type'].map(deadline_map)

# Late filing indicator
filings['late_filing'] = (
    filings['reporting_lag'] > filings['deadline_days']
).astype(int)

# Days relative to deadline (negative = early, positive = late)
filings['days_relative_deadline'] = (
    filings['reporting_lag'] - filings['deadline_days']
)

# Summary statistics
annual_filings = filings[filings['report_type'] == 'annual'].copy()
print("Annual Report Filing Lag (calendar days):")
print(annual_filings['reporting_lag'].describe().round(1))
print(f"\nLate filing rate: {annual_filings['late_filing'].mean():.1%}")

30.3.4 Phân phối độ trễ nộp hồ sơ

Plot the distribution of annual report filing lags
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Histogram
axes[0].hist(
    annual_filings['reporting_lag'].dropna(),
    bins=60, range=(20, 150),
    color='#2C5F8A', edgecolor='white', alpha=0.85
)
axes[0].axvline(x=90, color='#C0392B', linestyle='--', linewidth=2,
                label='90-day deadline')
axes[0].set_xlabel('Reporting Lag (Calendar Days)')
axes[0].set_ylabel('Number of Filings')
axes[0].set_title('Distribution of Annual Report Filing Lags')
axes[0].legend()

# Time trend: median lag by year
median_lag = (
    annual_filings
    .groupby('fiscal_year')['reporting_lag']
    .agg(['median', lambda x: x.quantile(0.25),
          lambda x: x.quantile(0.75)])
)
median_lag.columns = ['median', 'p25', 'p75']

axes[1].fill_between(
    median_lag.index, median_lag['p25'], median_lag['p75'],
    alpha=0.3, color='#2C5F8A', label='IQR'
)
axes[1].plot(
    median_lag.index, median_lag['median'],
    color='#2C5F8A', linewidth=2, marker='o', label='Median'
)
axes[1].axhline(y=90, color='#C0392B', linestyle='--',
                linewidth=1.5, label='Deadline')
axes[1].set_xlabel('Fiscal Year')
axes[1].set_ylabel('Filing Lag (Calendar Days)')
axes[1].set_title('Median Annual Filing Lag Over Time')
axes[1].legend()

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

30.4 Đo lường chất lượng công bố thông tin

Chất lượng công bố thông tin vốn có tính đa chiều. Theo Dechow, Ge, và Schrand (2010) và Beyer và c.s. (2010), chúng ta xây dựng các thước đo thay thế theo bốn chiều: (i) tính kịp thời, (ii) đặc tính văn bản, (iii) chất lượng kế toán, và (iv) độ rộng của công bố thông tin tự nguyện.

30.4.1 Tính kịp thời như một chiều của chất lượng

Công bố kịp thời làm giảm thời gian tồn tại bất cân xứng thông tin giữa người trong cuộc và nhà đầu tư bên ngoài. Chambers và Penman (1984) và Givoly và Palmon (1982) xác lập rằng các doanh nghiệp báo cáo sớm có xu hướng công bố tin tốt, trong khi các doanh nghiệp báo cáo muộn thường đưa ra tin xấu nhiều hơn. Chúng ta kiểm tra mô hình này trong bối cảnh Việt Nam ở phần dưới đây.

Chúng ta thao tác hóa tính kịp thời qua hai thước đo:

  1. Độ trễ báo cáo (liên tục): Số ngày theo lịch từ ngày kết thúc kỳ tài chính đến ngày nộp hồ sơ, được xây dựng như trong Mục 30.3.3.
  2. Phân loại sớm/muộn (phân loại): Chúng ta phân loại các quan sát doanh nghiệp–năm thành các nhóm tam phân vị dựa trên độ trễ báo cáo trong từng năm tài chính. Cách làm này kiểm soát các xu hướng dài hạn về tốc độ nộp hồ sơ (ví dụ do thay đổi quy định hoặc gián đoạn vì COVID-19).
Classify filings into timeliness terciles
annual_filings['lag_tercile'] = (
    annual_filings
    .groupby('fiscal_year')['reporting_lag']
    .transform(lambda x: pd.qcut(x, 3, labels=['Early', 'Middle', 'Late']))
)

# Tabulate
tercile_stats = (
    annual_filings
    .groupby('lag_tercile')['reporting_lag']
    .agg(['count', 'mean', 'median', 'std'])
    .round(1)
)
tercile_stats.columns = ['N', 'Mean Lag', 'Median Lag', 'SD']
print(tercile_stats)

30.4.2 Các thước đo chất lượng văn bản

Các đặc tính văn bản của thông tin công bố doanh nghiệp truyền tải thông tin về chất lượng vượt ra ngoài những gì các con số kế toán đơn thuần nắm bắt được. Li (2008) chứng minh rằng các báo cáo năm khó đọc hơn gắn với tính bền vững lợi nhuận thấp hơn, cho thấy ngôn ngữ phức tạp có thể che giấu thông tin bất lợi. Loughran và McDonald (2014) phê phán việc áp dụng các công thức đo mức độ dễ đọc thông dụng (chỉ số Fog, Flesch-Kincaid) cho văn bản tài chính, lập luận rằng các thước đo này lẫn lộn độ phức tạp với thuật ngữ chuyên môn.

Chúng ta xây dựng ba thước đo chất lượng văn bản được điều chỉnh cho thông tin công bố của doanh nghiệp Việt Nam:

30.4.2.1 Độ dài và tính cụ thể của tài liệu

Thông tin công bố dài hơn không nhất thiết tốt hơn, vì độ dài có thể phản ánh văn bản mẫu rập khuôn hoặc sự làm mờ. Tuy nhiên, Dyer, Lang, và Stice-Lawrence (2017) chỉ ra rằng thành phần có thông tin của việc công bố (khác với ngôn ngữ pháp lý tiêu chuẩn) đã tăng theo thời gian trong các báo cáo 10-K của Hoa Kỳ. Chúng ta đo lường:

  • Tổng số từ: Độ dài thô của các phần tường thuật trong báo cáo năm (tương đương MD&A)
  • Mật độ số liệu: Tỷ lệ các token là số, tỷ lệ phần trăm hoặc giá trị tiền tệ, là thước đo thay thế cho tính cụ thể.
Compute textual disclosure quality measures
import re
from underthesea import word_tokenize

def compute_textual_metrics(text):
    """Compute textual quality metrics for Vietnamese corporate text."""
    if not text or len(text.strip()) == 0:
        return {
            'word_count': 0, 'sentence_count': 0,
            'numerical_density': 0, 'avg_sentence_length': 0,
            'unique_word_ratio': 0, 'forward_looking_density': 0
        }

    # Vietnamese word segmentation
    tokens = word_tokenize(text)
    sentences = re.split(r'[.!?。]', text)
    sentences = [s.strip() for s in sentences if len(s.strip()) > 5]

    word_count = len(tokens)
    sentence_count = max(len(sentences), 1)

    # Numerical density: proportion of tokens that are numeric
    num_pattern = re.compile(r'^[\d,.%]+$')
    numeric_tokens = sum(1 for t in tokens if num_pattern.match(t))
    numerical_density = numeric_tokens / max(word_count, 1)

    # Lexical diversity: unique words / total words
    unique_words = len(set(t.lower() for t in tokens))
    unique_word_ratio = unique_words / max(word_count, 1)

    # Forward-looking statement density
    forward_keywords = [
        'dự kiến', 'kế hoạch', 'mục tiêu', 'triển vọng',
        'định hướng', 'chiến lược', 'tương lai', 'sẽ',
        'dự báo', 'phấn đấu', 'cam kết', 'hướng tới'
    ]
    text_lower = text.lower()
    forward_count = sum(text_lower.count(kw) for kw in forward_keywords)
    forward_looking_density = forward_count / max(sentence_count, 1)

    return {
        'word_count': word_count,
        'sentence_count': sentence_count,
        'numerical_density': numerical_density,
        'avg_sentence_length': word_count / sentence_count,
        'unique_word_ratio': unique_word_ratio,
        'forward_looking_density': forward_looking_density
    }

# Retrieve annual report text from DataCore
annual_text = client.get_annual_report_text(
    exchanges=['HOSE', 'HNX'],
    start_date='2012-01-01',
    end_date='2024-12-31',
    sections=['mda', 'business_overview', 'risk_factors']
)

# Apply textual metrics
textual_metrics = annual_text.apply(
    lambda row: compute_textual_metrics(row['text']),
    axis=1, result_type='expand'
)
annual_text = pd.concat([annual_text, textual_metrics], axis=1)

print("Textual Quality Summary Statistics:")
print(annual_text[['word_count', 'numerical_density',
                    'avg_sentence_length', 'unique_word_ratio',
                    'forward_looking_density']].describe().round(3))

30.4.2.2 Mật độ các phát biểu hướng tới tương lai

Forward-looking statements reveal management’s expectations about future performance and are considered a higher-quality form of disclosure because they expose the manager to ex-post evaluation. In Vietnamese reports, forward-looking language typically appears in the form of phrases like dự kiến (expected), kế hoạch (plan), mục tiêu (target), and triển vọng (outlook).

Guay, Samuels, và Taylor (2016) chỉ ra rằng nhà quản lý dùng công bố thông tin tự nguyện để “dẫn đường xuyên màn sương” khi báo cáo tài chính phức tạp. Chúng ta thao tác hóa mật độ hướng tới tương lai là số cụm từ hướng tới tương lai trên mỗi câu, theo cách tiếp cận từ khóa trong hàm compute_textual_metrics ở trên.

30.4.3 Các thước đo chất lượng dựa trên kế toán

Chúng ta bổ sung các thước đo văn bản bằng các thước đo thay thế dựa trên kế toán, nắm bắt độ tin cậy của thông tin tài chính được báo cáo.

30.4.3.1 Chất lượng các khoản dồn tích

Theo Francis và c.s. (2005), chúng ta đo chất lượng các khoản dồn tích bằng độ lệch chuẩn của phần dư từ hồi quy các khoản dồn tích vốn lưu động theo dòng tiền hoạt động trong quá khứ, hiện tại và tương lai:

\[ \frac{WC_{i,t}}{A_{i,t-1}} = \alpha + \beta_1 \frac{CFO_{i,t-1}}{A_{i,t-1}} + \beta_2 \frac{CFO_{i,t}}{A_{i,t-1}} + \beta_3 \frac{CFO_{i,t+1}}{A_{i,t-1}} + \varepsilon_{i,t} \tag{30.1}\]

trong đó \(WC_{i,t}\) là các khoản dồn tích vốn lưu động, \(CFO_{i,t}\) là dòng tiền hoạt động, và \(A_{i,t-1}\) là tổng tài sản trễ một kỳ. Độ lệch chuẩn cấp doanh nghiệp của \(\hat{\varepsilon}_{i,t}\) trên một cửa sổ trượt (thường là 5 năm) là thước đo chất lượng các khoản dồn tích, với giá trị càng cao cho thấy chất lượng càng thấp.

Estimate accruals quality (Dechow-Dichev residual volatility)
def estimate_accruals_quality(df, min_obs=5):
    """
    Estimate accruals quality as std dev of DD residuals
    over a rolling 5-year window for each firm.
    """
    results = []

    for ticker, group in df.groupby('ticker'):
        group = group.sort_values('fiscal_year')

        # Construct leads/lags of CFO
        group['cfo_lag1'] = group['operating_cash_flow'].shift(1)
        group['cfo_lead1'] = group['operating_cash_flow'].shift(-1)

        # Scale by lagged assets
        group['lag_assets'] = group['total_assets'].shift(1)
        for col in ['total_accruals', 'operating_cash_flow',
                     'cfo_lag1', 'cfo_lead1']:
            group[f'{col}_scaled'] = group[col] / group['lag_assets']

        # Rolling 5-year residual std dev
        for idx in range(len(group)):
            window = group.iloc[max(0, idx - 4):idx + 1]
            window = window.dropna(subset=[
                'total_accruals_scaled', 'operating_cash_flow_scaled',
                'cfo_lag1_scaled', 'cfo_lead1_scaled'
            ])

            if len(window) >= min_obs:
                y = window['total_accruals_scaled']
                X = sm.add_constant(window[[
                    'cfo_lag1_scaled',
                    'operating_cash_flow_scaled',
                    'cfo_lead1_scaled'
                ]])
                try:
                    model = sm.OLS(y, X).fit()
                    results.append({
                        'ticker': ticker,
                        'fiscal_year': group.iloc[idx]['fiscal_year'],
                        'accruals_quality': model.resid.std()
                    })
                except Exception:
                    pass

    return pd.DataFrame(results)

aq_df = estimate_accruals_quality(financials)
print(f"Accruals quality computed for {aq_df['ticker'].nunique()} firms")
print(aq_df['accruals_quality'].describe().round(4))

30.4.3.2 Tính bền vững và khả năng dự báo của lợi nhuận

Lợi nhuận bền vững hữu ích hơn cho định giá. Chúng ta ước lượng tính bền vững của lợi nhuận bằng hệ số góc \(\phi_1\) từ một mô hình tự hồi quy bậc nhất:

\[ \frac{E_{i,t}}{A_{i,t-1}} = \phi_0 + \phi_1 \frac{E_{i,t-1}}{A_{i,t-2}} + \nu_{i,t} \tag{30.2}\]

\(\hat{\phi}_1\) càng cao cho thấy lợi nhuận càng bền vững (và có thể cho là chất lượng càng cao).

Estimate firm-level earnings persistence
def estimate_persistence(df, min_obs=5):
    """Estimate earnings persistence via AR(1) model."""
    results = []

    for ticker, group in df.groupby('ticker'):
        group = group.sort_values('fiscal_year')
        group['earnings_scaled'] = group['net_income'] / group['total_assets'].shift(1)
        group['earnings_lag'] = group['earnings_scaled'].shift(1)

        clean = group.dropna(subset=['earnings_scaled', 'earnings_lag'])
        if len(clean) >= min_obs:
            y = clean['earnings_scaled']
            X = sm.add_constant(clean[['earnings_lag']])
            model = sm.OLS(y, X).fit()
            results.append({
                'ticker': ticker,
                'persistence': model.params['earnings_lag'],
                'persistence_se': model.bse['earnings_lag'],
                'r_squared': model.rsquared,
                'n_obs': model.nobs
            })

    return pd.DataFrame(results)

persistence_df = estimate_persistence(financials)
print(persistence_df[['persistence', 'r_squared']].describe().round(3))

30.4.4 Chỉ số tổng hợp về chất lượng công bố thông tin

Mỗi thước đo chất lượng riêng lẻ nắm bắt một khía cạnh khác nhau của môi trường thông tin. Để tổng hợp chúng thành một điểm số duy nhất mà tránh được việc đặt trọng số tùy tiện, chúng ta theo Lang và Lundholm (1993) và sử dụng một chỉ số tổng hợp dựa trên thứ hạng. Với mỗi quan sát doanh nghiệp–năm, chúng ta xếp hạng các doanh nghiệp theo từng chiều sau (thứ hạng cao hơn = chất lượng cao hơn) (Bảng 30.2).

Bảng 30.2: Các thành phần của chỉ số tổng hợp về chất lượng công bố thông tin.
Chiều Thước đo thay thế Hướng
Tính kịp thời Độ trễ báo cáo Thấp hơn thì tốt hơn
Tính cụ thể Mật độ số liệu Cao hơn thì tốt hơn
Hướng tới tương lai Mật độ FLS Cao hơn thì tốt hơn
Chất lượng lợi nhuận Chất lượng dồn tích (DD) σ thấp hơn thì tốt hơn
Tính bền vững Hệ số AR(1) Cao hơn thì tốt hơn

Chúng ta chuyển mỗi thước đo thay thế thành thứ hạng phân vị trong từng năm tài chính (để mỗi thành phần nằm trong khoảng từ 0 đến 1), rồi lấy trung bình qua các thành phần:

\[ DQ_{i,t} = \frac{1}{K} \sum_{k=1}^{K} \text{Rank}_{k,i,t} \tag{30.3}\]

trong đó \(K\) là số thành phần có sẵn và \(\text{Rank}_{k,i,t}\) là thứ hạng phân vị của doanh nghiệp \(i\) trong năm \(t\) theo chiều \(k\).

Construct composite disclosure quality index
# Merge all quality proxies
quality_panel = (
    annual_filings[['ticker', 'fiscal_year', 'reporting_lag']]
    .merge(
        annual_text[['ticker', 'fiscal_year', 'numerical_density',
                      'forward_looking_density']],
        on=['ticker', 'fiscal_year'], how='left'
    )
    .merge(aq_df, on=['ticker', 'fiscal_year'], how='left')
    .merge(persistence_df[['ticker', 'persistence']],
           on='ticker', how='left')
)

# Rank each component within fiscal year (higher = better quality)
def year_percentile_rank(series):
    """Convert to percentile rank within group."""
    return series.rank(pct=True)

rank_cols = {}
for col, ascending in [
    ('reporting_lag', False),       # lower lag = better → invert
    ('numerical_density', True),    # higher = better
    ('forward_looking_density', True),
    ('accruals_quality', False),    # lower volatility = better → invert
    ('persistence', True)           # higher = better
]:
    col_to_rank = quality_panel[col] if ascending else -quality_panel[col]
    rank_cols[f'rank_{col}'] = (
        quality_panel
        .groupby('fiscal_year')[col]
        .transform(lambda x: x.rank(pct=True) if ascending
                   else (-x).rank(pct=True))
    )

rank_df = pd.DataFrame(rank_cols)
quality_panel = pd.concat([quality_panel, rank_df], axis=1)

# Composite index: average of available ranks
rank_columns = [c for c in quality_panel.columns if c.startswith('rank_')]
quality_panel['dq_index'] = quality_panel[rank_columns].mean(axis=1)

print("Disclosure Quality Index Distribution:")
print(quality_panel['dq_index'].describe().round(3))
Plot distribution of composite disclosure quality index
fig, ax = plt.subplots(figsize=(10, 5))
ax.hist(quality_panel['dq_index'].dropna(), bins=50,
        color='#2C5F8A', edgecolor='white', alpha=0.85)
ax.axvline(quality_panel['dq_index'].median(), color='#E67E22',
           linestyle='--', linewidth=2, label='Median')
ax.set_xlabel('Disclosure Quality Index')
ax.set_ylabel('Number of Firm-Years')
ax.set_title('Distribution of Composite Disclosure Quality')
ax.legend()
plt.tight_layout()
plt.show()
Hình 30.2

30.5 Các yếu tố quyết định chất lượng công bố thông tin

Điều gì thúc đẩy sự khác biệt về chất lượng công bố thông tin giữa các doanh nghiệp Việt Nam? Chúng ta ước lượng một hồi quy chéo của chỉ số DQ tổng hợp theo các đặc điểm doanh nghiệp và các biến quản trị:

\[ DQ_{i,t} = \alpha + \beta_1 \ln(\text{Size}_{i,t}) + \beta_2 \text{ROA}_{i,t} + \beta_3 \text{Lev}_{i,t} + \beta_4 \text{StateOwn}_{i,t} + \beta_5 \text{ForeignOwn}_{i,t} + \beta_6 \text{Big4}_{i,t} + \beta_7 \text{BoardIndep}_{i,t} + \gamma_t + \varepsilon_{i,t} \tag{30.4}\]

trong đó \(\gamma_t\) là các hiệu ứng cố định theo năm.

Các dự báo lý thuyết, dựa trên Lang và Lundholm (1993), Hope (2003) và Bushman và c.s. (2004), là:

  • Quy mô (+): Các doanh nghiệp lớn hơn chịu sự giám sát công chúng lớn hơn và có chi phí độc quyền thông tin thấp hơn so với lợi ích của việc công bố thông tin.
  • ROA (+/−): Các doanh nghiệp có lợi nhuận cao có thể công bố nhiều hơn để phát tín hiệu chất lượng, nhưng các doanh nghiệp điều chỉnh lợi nhuận giảm xuống (vì mục đích thuế) có thể giảm công bố để tránh bị soi xét.
  • Đòn bẩy (+): Sengupta (1998) lập luận rằng các doanh nghiệp có nhiều nợ hơn có động cơ mạnh hơn để duy trì chất lượng công bố thông tin nhằm giảm chi phí vay.
  • Sở hữu nhà nước (−): Các doanh nghiệp nhà nước (SOE) có thể chịu kỷ luật thị trường yếu hơn và có động cơ chính trị để hạn chế tính minh bạch.
  • Sở hữu nước ngoài (+): Các nhà đầu tư tổ chức nước ngoài đòi hỏi tính minh bạch cao hơn.
  • Công ty kiểm toán Big 4 (+): Các công ty kiểm toán chất lượng cao hạn chế quản trị lợi nhuận và gián tiếp cải thiện chất lượng công bố thông tin.
  • Tính độc lập của hội đồng quản trị (+): Các thành viên độc lập cải thiện việc giám sát và khuyến khích công bố thông tin hữu ích hơn.
Estimate determinants of disclosure quality
# Merge quality index with financials and governance
det_panel = (
    quality_panel[['ticker', 'fiscal_year', 'dq_index']]
    .merge(financials, on=['ticker', 'fiscal_year'], how='left')
    .merge(governance, on=['ticker', 'fiscal_year'], how='left')
)

# Construct variables
det_panel['log_size'] = np.log(det_panel['total_assets'])
det_panel['roa'] = det_panel['net_income'] / det_panel['total_assets']
det_panel['leverage'] = (
    (det_panel['total_assets'] - det_panel['total_equity'])
    / det_panel['total_assets']
)

# Panel regression with year FE
det_panel = det_panel.set_index(['ticker', 'fiscal_year'])

model_det = PanelOLS(
    dependent=det_panel['dq_index'],
    exog=sm.add_constant(det_panel[[
        'log_size', 'roa', 'leverage',
        'state_ownership_pct', 'foreign_ownership_pct',
        'big4_auditor', 'board_independence_pct'
    ]]),
    entity_effects=False,
    time_effects=True,
    check_rank=False
).fit(cov_type='clustered', cluster_entity=True)

print(model_det.summary)
Visualize determinant regression coefficients
coefs = model_det.params.drop('const')
ci = model_det.conf_int().drop('const')

fig, ax = plt.subplots(figsize=(8, 5))
y_pos = range(len(coefs))
labels = [
    'ln(Assets)', 'ROA', 'Leverage', 'State Own %',
    'Foreign Own %', 'Big 4 Auditor', 'Board Indep %'
]

colors = ['#2C5F8A' if c > 0 else '#C0392B' for c in coefs.values]
ax.barh(y_pos, coefs.values, color=colors, alpha=0.8, height=0.6)
ax.errorbar(
    coefs.values, y_pos,
    xerr=[coefs.values - ci.iloc[:, 0].values,
          ci.iloc[:, 1].values - coefs.values],
    fmt='none', color='black', capsize=3
)
ax.axvline(x=0, color='gray', linewidth=0.8, linestyle='-')
ax.set_yticks(y_pos)
ax.set_yticklabels(labels)
ax.set_xlabel('Coefficient Estimate')
ax.set_title('Determinants of Disclosure Quality')
plt.tight_layout()
plt.show()
Hình 30.3

30.6 Thời điểm công bố thông tin mang tính chiến lược

30.6.1 Hiệu ứng ngày trong tuần

DellaVigna và Pollet (2009) ghi nhận rằng các thông báo lợi nhuận vào thứ Sáu nhận được ít sự chú ý tức thời của thị trường hơn. Chúng ta kiểm tra xem mô hình này có đúng ở Việt Nam hay không, nơi tuần giao dịch kéo dài từ thứ Hai đến thứ Sáu nhưng cơ sở nhà đầu tư do cá nhân chi phối có thể thể hiện các mô hình chú ý khác.

Analyze day-of-week patterns in earnings announcements
annual_filings['announcement_dow'] = (
    annual_filings['announcement_date'].dt.dayofweek
)
annual_filings['day_name'] = (
    annual_filings['announcement_date'].dt.day_name()
)

# Compute surprise: actual earnings minus naive expectation (last year's earnings)
annual_filings = annual_filings.merge(
    financials[['ticker', 'fiscal_year', 'net_income', 'total_assets']],
    on=['ticker', 'fiscal_year'], how='left'
)
annual_filings['earnings_scaled'] = (
    annual_filings['net_income'] / annual_filings['total_assets']
)
annual_filings['earnings_surprise'] = (
    annual_filings
    .groupby('ticker')['earnings_scaled']
    .diff()
)

# Classify as good/bad news
annual_filings['bad_news'] = (
    annual_filings['earnings_surprise'] < 0
).astype(int)

# Day-of-week distribution by news type
dow_crosstab = pd.crosstab(
    annual_filings['day_name'],
    annual_filings['bad_news'].map({0: 'Good News', 1: 'Bad News'}),
    normalize='columns'
)
# Reorder days
day_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']
dow_crosstab = dow_crosstab.reindex(day_order)

print("Proportion of Announcements by Day and News Type:")
print(dow_crosstab.round(3))
Plot day-of-week announcement patterns
fig, ax = plt.subplots(figsize=(10, 5))
x = np.arange(len(day_order))
width = 0.35

bad_pct = dow_crosstab['Bad News'].values
good_pct = dow_crosstab['Good News'].values

ax.bar(x - width/2, good_pct, width, label='Good News',
       color='#27AE60', alpha=0.8)
ax.bar(x + width/2, bad_pct, width, label='Bad News',
       color='#C0392B', alpha=0.8)
ax.set_xticks(x)
ax.set_xticklabels(day_order)
ax.set_ylabel('Proportion of Announcements')
ax.set_title('Strategic Timing: Day-of-Week Announcement Patterns')
ax.legend()
plt.tight_layout()
plt.show()
Hình 30.4

30.6.2 Sự ùn tắc thông báo

Khi nhiều doanh nghiệp thông báo cùng một ngày, mỗi thông báo nhận được ít sự chú ý hơn. Chúng ta đo mức ùn tắc thông báo bằng số doanh nghiệp khác công bố lợi nhuận vào cùng ngày:

\[ \text{Congestion}_{i,t} = \sum_{j \neq i} \mathbf{1}\{\text{AnnDate}_{j} = \text{AnnDate}_{i}\} \tag{30.5}\]

Hirshleifer, Lim, và Teoh (2009) dự báo rằng các doanh nghiệp chôn tin xấu sẽ chọn những ngày ùn tắc cao. Chúng ta kiểm tra điều này bằng cách hồi quy biến ùn tắc theo dấu của tin tức lợi nhuận:

Test whether bad-news firms choose high-congestion announcement days
# Count announcements per date
ann_counts = (
    annual_filings
    .groupby('announcement_date')
    .size()
    .reset_index(name='n_announcements')
)
annual_filings = annual_filings.merge(
    ann_counts, on='announcement_date', how='left'
)
annual_filings['congestion'] = annual_filings['n_announcements'] - 1

# Regression: congestion ~ bad_news + controls
congestion_model = smf.ols(
    'congestion ~ bad_news + log_size + roa + C(fiscal_year)',
    data=annual_filings.assign(
        log_size=np.log(annual_filings['total_assets']),
        roa=annual_filings['net_income'] / annual_filings['total_assets']
    )
).fit(cov_type='cluster', cov_kwds={'groups': annual_filings['ticker']})

print("Congestion Regression:")
print(congestion_model.summary().tables[1])

30.6.3 Thông báo ngoài giờ và cuối tuần

Quy định của Việt Nam yêu cầu công bố trong vòng 24 giờ kể từ các sự kiện trọng yếu, nhưng doanh nghiệp vẫn có quyền tùy nghi về thời điểm chính xác. Các thông báo được đưa ra sau khi phiên giao dịch kết thúc (sau 3:00 chiều trên HOSE/HNX) hoặc vào cuối tuần làm chậm cơ hội phản ứng của thị trường ít nhất một ngày giao dịch.

Identify and analyze after-hours announcement patterns
# Assume announcement timestamps are available
annual_filings['ann_hour'] = (
    annual_filings['announcement_date'].dt.hour
)
annual_filings['after_hours'] = (
    (annual_filings['ann_hour'] >= 15) |
    (annual_filings['announcement_dow'] >= 5)  # Saturday/Sunday
).astype(int)

# Cross-tabulate after-hours by news type
afterhours_crosstab = pd.crosstab(
    annual_filings['after_hours'].map({0: 'During Hours', 1: 'After Hours'}),
    annual_filings['bad_news'].map({0: 'Good News', 1: 'Bad News'}),
    normalize='index'
)
print("News Distribution by Announcement Timing:")
print(afterhours_crosstab.round(3))

# Chi-squared test
contingency = pd.crosstab(
    annual_filings['after_hours'], annual_filings['bad_news']
)
chi2, p_val, _, _ = stats.chi2_contingency(contingency)
print(f"\nChi-squared = {chi2:.2f}, p-value = {p_val:.4f}")

30.7 Hệ quả thị trường của chất lượng công bố thông tin

30.7.1 Chất lượng công bố thông tin và chi phí vốn cổ phần

Dự báo trung tâm của Diamond và Verrecchia (1991) và Botosan (1997) là công bố thông tin chất lượng cao hơn làm giảm chi phí vốn cổ phần bằng cách giảm bất cân xứng thông tin. Chúng ta kiểm tra điều này bằng cách tiếp cận chi phí vốn ngụ ý (ICC), trong đó chúng ta ước lượng tỷ lệ chiết khấu làm cho giá hiện tại bằng giá trị hiện tại của lợi nhuận kỳ vọng trong tương lai.

Chúng ta dùng cách tiếp cận tỷ số PEG làm ước tính ICC đơn giản:

\[ r_{PEG,i,t} = \sqrt{\frac{\hat{E}_{i,t+2} - \hat{E}_{i,t+1}}{P_{i,t}}} \tag{30.6}\]

trong đó \(\hat{E}_{i,t+k}\) là dự báo lợi nhuận đồng thuận (hoặc, khi không có nhà phân tích theo dõi, là dự báo dựa trên mô hình) và \(P_{i,t}\) là giá cổ phiếu hiện tại.

Estimate implied cost of equity and test disclosure quality effect
# Construct earnings forecasts using a simple random walk with drift
forecasts = financials.sort_values(['ticker', 'fiscal_year']).copy()
forecasts['eps'] = forecasts['net_income'] / forecasts['market_cap']
forecasts['eps_growth'] = forecasts.groupby('ticker')['eps'].pct_change()

# Simple forecast: E[t+1] = E[t] * (1 + avg_growth)
forecasts['avg_growth'] = (
    forecasts.groupby('ticker')['eps_growth']
    .transform(lambda x: x.rolling(3, min_periods=2).mean())
)
forecasts['eps_f1'] = forecasts['eps'] * (1 + forecasts['avg_growth'])
forecasts['eps_f2'] = forecasts['eps_f1'] * (1 + forecasts['avg_growth'])

# PEG-based ICC
forecasts['icc_peg'] = np.sqrt(
    np.maximum(forecasts['eps_f2'] - forecasts['eps_f1'], 0)
    / np.maximum(forecasts['market_cap'] / 1e6, 1e-6)
)

# Merge with disclosure quality
icc_panel = (
    forecasts[['ticker', 'fiscal_year', 'icc_peg']]
    .merge(quality_panel[['ticker', 'fiscal_year', 'dq_index']].reset_index(drop=True),
           on=['ticker', 'fiscal_year'], how='inner')
    .merge(governance, on=['ticker', 'fiscal_year'], how='left')
    .merge(financials[['ticker', 'fiscal_year', 'total_assets',
                        'book_to_market', 'market_cap']],
           on=['ticker', 'fiscal_year'], how='left')
)

icc_panel['log_size'] = np.log(icc_panel['market_cap'])
icc_panel = icc_panel.set_index(['ticker', 'fiscal_year'])

# Panel regression: ICC ~ DQ + controls
icc_model = PanelOLS(
    dependent=icc_panel['icc_peg'],
    exog=sm.add_constant(icc_panel[[
        'dq_index', 'log_size', 'book_to_market'
    ]]),
    entity_effects=True,
    time_effects=True,
    check_rank=False
).fit(cov_type='clustered', cluster_entity=True)

print("Implied Cost of Capital ~ Disclosure Quality:")
print(icc_model.summary)

30.7.2 Chất lượng công bố thông tin và thanh khoản

Diamond và Verrecchia (1991) dự báo rằng công bố thông tin tốt hơn làm giảm lựa chọn đối nghịch và cải thiện thanh khoản. Chúng ta đo thanh khoản qua chênh lệch giá mua–bán và tỷ số phi thanh khoản Amihud:

\[ \text{Amihud}_{i,t} = \frac{1}{D_{i,t}} \sum_{d=1}^{D_{i,t}} \frac{|R_{i,d}|}{\text{Volume}_{i,d}} \tag{30.7}\]

trong đó \(R_{i,d}\) là lợi nhuận hằng ngày và \(\text{Volume}_{i,d}\) là khối lượng giao dịch hằng ngày tính bằng VND.

Compute Amihud illiquidity and test disclosure quality effect
# Compute annual Amihud illiquidity
trading['abs_return'] = trading['close'].pct_change().abs()
trading['amihud_daily'] = trading['abs_return'] / (trading['volume'] * trading['close'])

amihud_annual = (
    trading
    .assign(fiscal_year=trading['date'].dt.year)
    .groupby(['ticker', 'fiscal_year'])
    .agg(
        amihud=('amihud_daily', 'mean'),
        avg_spread=('bid_ask_spread', 'mean'),
        avg_turnover=('turnover', 'mean')
    )
    .reset_index()
)

# Log transform for better distributional properties
amihud_annual['log_amihud'] = np.log(amihud_annual['amihud'] + 1e-10)
amihud_annual['log_spread'] = np.log(amihud_annual['avg_spread'] + 1e-6)

# Merge and run regression
liq_panel = (
    amihud_annual
    .merge(quality_panel[['ticker', 'fiscal_year', 'dq_index']].reset_index(drop=True),
           on=['ticker', 'fiscal_year'], how='inner')
    .merge(financials[['ticker', 'fiscal_year', 'market_cap', 'total_assets']],
           on=['ticker', 'fiscal_year'], how='left')
)
liq_panel['log_size'] = np.log(liq_panel['market_cap'])
liq_panel = liq_panel.set_index(['ticker', 'fiscal_year'])

liq_model = PanelOLS(
    dependent=liq_panel['log_amihud'],
    exog=sm.add_constant(liq_panel[['dq_index', 'log_size']]),
    entity_effects=True,
    time_effects=True,
    check_rank=False
).fit(cov_type='clustered', cluster_entity=True)

print("Amihud Illiquidity ~ Disclosure Quality:")
print(liq_model.summary)
Plot disclosure quality vs liquidity relationship
liq_panel_plot = liq_panel.reset_index()
liq_panel_plot['dq_quintile'] = pd.qcut(
    liq_panel_plot['dq_index'], 5, labels=['Q1\n(Low)', 'Q2', 'Q3', 'Q4', 'Q5\n(High)']
)

quintile_liq = (
    liq_panel_plot
    .groupby('dq_quintile')['log_amihud']
    .agg(['mean', 'sem'])
)

fig, ax = plt.subplots(figsize=(8, 5))
bars = ax.bar(
    range(5), quintile_liq['mean'],
    yerr=1.96 * quintile_liq['sem'],
    color=['#C0392B', '#E67E22', '#F1C40F', '#27AE60', '#2C5F8A'],
    alpha=0.85, capsize=4, edgecolor='white'
)
ax.set_xticks(range(5))
ax.set_xticklabels(quintile_liq.index)
ax.set_xlabel('Disclosure Quality Quintile')
ax.set_ylabel('Log Amihud Illiquidity')
ax.set_title('Disclosure Quality and Market Liquidity')
plt.tight_layout()
plt.show()
Hình 30.5

30.7.2.1 Sai số đo lường trong biến kết quả thanh khoản

Phương trình 37.2 dùng một lợi nhuận tuyệt đối hằng ngày duy nhất để tóm tắt toàn bộ biến động giá trong một ngày. Như đã trình bày chi tiết trong chương về thanh khoản (Mục 14.4), tử số đó mang sai số đo lường tương đối khoảng 76% mỗi ngày, và Lacava, Ranaldo, và Santucci de Magistris (2026) chỉ ra rằng có thể thay nó bằng một ước lượng tần suất cao của cùng đại lượng đó, gọi là Amihud thực hiện (realized Amihud),

\[ \text{RAmihud}_{i,t} = \frac{1}{D_{i,t}}\sum_{d=1}^{D_{i,t}} \sqrt{\frac{2}{\pi}}\,\frac{\sqrt{\text{RV}_{i,d}}}{\text{Volume}_{i,d}}, \qquad \text{RV}_{i,d} = \sum_{j=1}^{M} r_{i,d,j}^2 \tag{30.8}\]

trong đó \(r_{i,d,j}\) là các lợi nhuận trong ngày và hằng số \(\sqrt{2/\pi}\) giữ cho Phương trình 30.8 cùng thang đo với Phương trình 37.2. Với \(M = 45\) khoảng thời gian năm phút của HOSE, sai số tương đối của tử số giảm từ khoảng 76% xuống khoảng 11%.

Người ta dễ kết luận rằng đặc tả này do đó đánh giá thấp tác động của chất lượng công bố thông tin. Không phải vậy, và lý do đáng được nêu chính xác, bởi vì kinh tế lượng ở đây khác về căn bản với ứng dụng phân loại danh mục trong chương về thanh khoản. Ở đó, phi thanh khoản là biến sắp xếp ở vế phải, và sai số đo lường làm suy giảm phần bù ước lượng về phía không. Ở đây, phi thanh khoản là biến phụ thuộc. Viết biến kết quả đo được là giá trị thật cộng nhiễu, \(\widehat{y}_i = y_i + \nu_i\), hồi quy \(\widehat{y}_i = \alpha + \beta\,\text{DQ}_i + (\varepsilon_i + \nu_i)\) có hệ số góc không đổi theo kỳ vọng với điều kiện \(\nu_i\) không tương quan với \(\text{DQ}_i\). Sai số đo lường cổ điển trong biến kết quả làm mất độ chính xác, chứ không làm mất tính vững (Hausman 2001). Do đó, thước đo thực hiện mang lại sức mạnh thống kê trong chương này, chứ không phải hiệu chỉnh thiên lệch.

Hai câu hỏi đặt ra. Sức mạnh thống kê tăng bao nhiêu, và điều kiện “không tương quan với \(\text{DQ}\)” có thực sự được thỏa mãn ở đây không?

30.7.2.2 Nơi nó hầu như không quan trọng: đặc tả theo năm

Hồi quy ở trên lấy trung bình các tỷ số hằng ngày trong cả năm tài chính. Chính việc lấy trung bình đã khắc chế sai số đo lường: trên \(D\) ngày, sai số tương đối của tử số Amihud giảm xuống \(0.7555/\sqrt{D}\), với một doanh nghiệp có thanh khoản tốt và 250 ngày giao dịch thì dưới 5%, đã đủ nhỏ để việc giảm thêm một nửa nữa thay đổi rất ít.

Tuy nhiên, có một kênh mà qua đó đặc tả theo năm có thể bị thiên lệch, và kênh này gắn riêng với phép biến đổi log mà chương này áp dụng. Vì \(\log\) là hàm lõm, \(\mathbb{E}[\log \widehat{y}] < \log \mathbb{E}[\widehat{y}]\), và độ lớn của khoảng cách Jensen đó xấp xỉ \(-\tfrac{1}{2}\,\text{relvar}/D\). Do đó nó thay đổi giữa các doanh nghiệp theo \(D_{i,t}\). Các doanh nghiệp giao dịch ít ngày chịu méo mó hướng xuống lớn hơn trong \(\log(\text{Amihud})\) so với các doanh nghiệp giao dịch mỗi ngày. Vì các doanh nghiệp giao dịch thưa thớt cũng chiếm tỷ lệ lớn trong nhóm công bố kém, rốt cuộc \(\nu_i\) tương quan với \(\text{DQ}_i\), và hệ số góc không còn sạch. Độ lớn là \(-0.285/D\) đối với thước đo cổ điển so với \(-0.0056/D\) đối với thước đo thực hiện, chênh nhau năm mươi lần.

Đoạn mã sau định lượng cả hai tác động. Nó mô phỏng một tác động chất lượng công bố thông tin đã biết là \(\beta = -0.10\) lên log phi thanh khoản, trước hết với số ngày giao dịch giống nhau giữa các doanh nghiệp rồi với số ngày giao dịch tăng theo chất lượng công bố thông tin.

Does numerator noise bias the annual disclosure-liquidity slope?
import numpy as np
import pandas as pd
from scipy.stats import norm

rng_dq = np.random.default_rng(4242)
M_HF, BETA_DQ, N_FIRMS, REPS_A, SD_E = 45, -0.10, 400, 300, 0.50


def measure_factor(kind, n, D, rng):
    """
    Multiplicative measurement-error factor in the Amihud numerator,
    averaged over each firm's D trading days. Mean one by construction;
    only the variance differs between the two measures.
    """
    Dm = D.max()
    mask = np.arange(Dm)[None, :] < D[:, None]
    if kind == "classic":
        z = np.abs(rng.normal(0, 1, (n, Dm))) / np.sqrt(2 / np.pi)   # half-normal
    else:
        z = np.sqrt(rng.chisquare(M_HF, (n, Dm)) / M_HF)             # sqrt(RV)
    return np.where(mask, z, 0).sum(1) / D


def annual_rep(link_days, rng):
    """One draw of the annual cross-sectional regression."""
    DQ = rng.normal(0, 1, N_FIRMS)
    log_true = 1.0 + BETA_DQ * DQ + rng.normal(0, SD_E, N_FIRMS)

    # Better disclosers trade on more days: 20 to 250 per year.
    D = (np.clip(np.round(20 + 230 * norm.cdf(DQ)), 20, 250).astype(int)
         if link_days else np.full(N_FIRMS, 250))

    X = np.column_stack([np.ones(N_FIRMS), DQ])
    XtXinv = np.linalg.inv(X.T @ X)

    out = {}
    for kind in ["classic", "realized", "truth"]:
        f = (np.ones(N_FIRMS) if kind == "truth"
             else measure_factor(kind, N_FIRMS, D, rng))
        y = log_true + np.log(f)
        b = XtXinv @ X.T @ y
        r = y - X @ b
        se = np.sqrt((r @ r / (N_FIRMS - 2)) * XtXinv[1, 1])
        out[kind] = (b[1], se, b[1] / se)
    return out


for link_days in [False, True]:
    acc = {k: [] for k in ["classic", "realized", "truth"]}
    for _ in range(REPS_A):
        for k, v in annual_rep(link_days, rng_dq).items():
            acc[k].append(v)

    rows = []
    for k, v in acc.items():
        a = np.array(v)
        rows.append({
            "measure": k,
            "mean beta": round(a[:, 0].mean(), 4),
            "attenuation %": round(100 * (1 - a[:, 0].mean() / BETA_DQ), 1),
            "mean |t|": round(np.abs(a[:, 2]).mean(), 2),
            "power@5%": round((np.abs(a[:, 2]) > 1.96).mean(), 3),
        })
    print(f"\nTrading days linked to disclosure quality: {link_days}")
    print(pd.DataFrame(rows).to_string(index=False))

Trading days linked to disclosure quality: False
 measure  mean beta  attenuation %  mean |t|  power@5%
 classic    -0.1002           -0.2      3.99     0.977
realized    -0.1002           -0.2      4.01     0.977
   truth    -0.1002           -0.2      4.01     0.977

Trading days linked to disclosure quality: True
 measure  mean beta  attenuation %  mean |t|  power@5%
 classic    -0.0972            2.8      3.81     0.973
realized    -0.1000           -0.0      3.97     0.987
   truth    -0.1001           -0.1      3.97     0.987

Kết quả là đáng yên tâm đối với đặc tả đã được ước lượng trong chương này. Khi số ngày giao dịch không liên quan đến chất lượng công bố thông tin, cả ba thước đo đều khôi phục \(\beta\) về cơ bản chính xác, xác nhận rằng sai số đo lường ở phía biến kết quả không làm thiên lệch hệ số góc. Khi số ngày giao dịch có liên hệ với chất lượng công bố thông tin, thước đo cổ điển bị suy giảm khoảng 3%, trong khi thước đo thực hiện và giá trị thật không khả thi là không thể phân biệt. Mức suy giảm 3% không phải là điều quyết định liệu mối quan hệ công bố–thanh khoản có được phát hiện hay không, và nó nằm thoải mái trong mọi khoảng tin cậy hợp lý.

Kết luận trung thực đối với hồi quy theo năm là Amihud thực hiện không đáng với dữ liệu trong ngày mà nó đòi hỏi. Biết một cải tiến không quan trọng ở đâu cũng hữu ích như biết nó quan trọng ở đâu; việc lấy trung bình theo năm trong Phương trình 37.2 đã làm phần lớn công việc mà lấy mẫu tần suất cao sẽ làm.

30.7.2.3 Nơi nó quan trọng rất nhiều: các cửa sổ sự kiện ngắn

Bức tranh hoàn toàn đảo ngược đối với các thiết kế nghiên cứu sự kiện ở những phần khác của chương này. Kiểm tra xem thanh khoản có cải thiện sau khi nộp hồ sơ hay không đòi hỏi so sánh phi thanh khoản trên một cửa sổ vài ngày ở hai phía của sự kiện, và cửa sổ ngắn chính là nơi việc lấy trung bình không thể cứu một tử số nhiễu. Trên cửa sổ năm ngày, sai số tương đối của tử số cổ điển là \(0.7555/\sqrt{5} \approx 34\%\); còn của tử số thực hiện dưới 5%.

Vì nhiễu đi vào hiệu số trước–sau hai lần, tác động của nó lên sức mạnh thống kê là nghiêm trọng. Mô phỏng dưới đây tính sức mạnh để phát hiện một thay đổi thực sự sau khi nộp hồ sơ trong log phi thanh khoản, cho phép sự không đồng nhất thực giữa các doanh nghiệp trong phản ứng để thước đo thực hiện không đạt sức mạnh tuyệt đối (bằng 1) một cách tầm thường.

Power to detect a post-filing liquidity change, by window length
rng_ev = np.random.default_rng(77)
REPS_B = 1500
RELVAR = {"classic": np.pi / 2 - 1, "realized": 1 / (2 * M_HF)}


def win_noise(kind, n, D):
    """Log measurement error in a D-day window average of the numerator."""
    z = (np.abs(rng_ev.normal(0, 1, (n, D))) / np.sqrt(2 / np.pi) if kind == "classic"
         else np.sqrt(rng_ev.chisquare(M_HF, (n, D)) / M_HF))
    return np.log(z.mean(1))


def ev_power(kind, n, D, delta, sd_delta):
    """Rejection rate of a paired pre/post test of a true change `delta`."""
    rejections = 0
    for _ in range(REPS_B):
        d = ((delta + rng_ev.normal(0, sd_delta, n))
             + win_noise(kind, n, D) - win_noise(kind, n, D))
        rejections += abs(d.mean() / (d.std(ddof=1) / np.sqrt(n))) > 1.96
    return rejections / REPS_B


SD_RESP = 0.10          # genuine cross-firm variation in the liquidity response
rows_ev = []
for D in [5, 10, 21]:
    for delta in [-0.02, -0.03, -0.05]:
        row = {"window (days)": D, "true change": delta}
        for kind in ["classic", "realized"]:
            row[f"power {kind}"] = round(ev_power(kind, 200, D, delta, SD_RESP), 3)
            # Closed-form firms needed for 80% power at the 5% level.
            var_d = SD_RESP ** 2 + 2 * RELVAR[kind] / D
            row[f"firms for 80% ({kind})"] = int(np.ceil(7.849 * var_d / delta ** 2))
        rows_ev.append(row)

ev = pd.DataFrame(rows_ev)
ev["sample-size ratio"] = (ev["firms for 80% (classic)"]
                           / ev["firms for 80% (realized)"]).round(1)
print("200 firms, 5% two-sided test, cross-firm response sd = 0.10")
print(ev.to_string(index=False))
200 firms, 5% two-sided test, cross-firm response sd = 0.10
 window (days)  true change  power classic  firms for 80% (classic)  power realized  firms for 80% (realized)  sample-size ratio
             5        -0.02          0.088                     4677           0.661                       284               16.5
             5        -0.03          0.121                     2079           0.949                       126               16.5
             5        -0.05          0.283                      749           0.999                        46               16.3
            10        -0.02          0.127                     2437           0.732                       240               10.2
            10        -0.03          0.238                     1083           0.967                       107               10.1
            10        -0.05          0.499                      390           1.000                        39               10.0
            21        -0.02          0.203                     1263           0.756                       217                5.8
            21        -0.03          0.383                      562           0.978                        97                5.8
            21        -0.05          0.775                      203           1.000                        35                5.8

Khoảng cách đủ lớn để thay đổi điều gì là khả thi. Với 200 doanh nghiệp và cửa sổ mười ngày, sức mạnh để phát hiện cải thiện thanh khoản 3% là khoảng 0.24 với thước đo cổ điển và khoảng 0.97 với thước đo thực hiện. Diễn đạt theo cỡ mẫu cần thiết, phát hiện tác động đó với sức mạnh 80% cần khoảng 1,080 doanh nghiệp với thước đo hằng ngày và khoảng 110 với thước đo thực hiện. Vì toàn bộ tập hợp doanh nghiệp niêm yết của Việt Nam chỉ cỡ 1,600 doanh nghiệp trên ba sàn, và một nghiên cứu về hồ sơ báo cáo năm giới hạn trong các doanh nghiệp có dữ liệu liên tục sẽ chỉ là một phần nhỏ trong đó, thước đo cổ điển khiến kiểm định này bất khả thi với hầu hết các mẫu con, còn thước đo thực hiện biến nó thành thường quy.

Ba nhận xét nữa rút ra từ bảng.

Lợi thế tăng lên khi cửa sổ thu hẹp. Tỷ lệ cỡ mẫu khoảng 5.8 ở 21 ngày, 10 ở mười ngày và 16.5 ở năm ngày. Vì các cửa sổ sự kiện hẹp nhất chính là nơi việc nhận diện sạch nhất, ít bị các tin tức gây nhiễu khác chi phối nhất, thước đo nhiễu áp đặt sự đánh đổi trực tiếp giữa sức mạnh thống kê và chất lượng nhận diện. Thước đo thực hiện nới lỏng sự đánh đổi này.

Kết quả không có ý nghĩa từ một kiểm định thanh khoản cửa sổ ngắn gần như không mang thông tin. Với 200 doanh nghiệp và cửa sổ năm ngày, thước đo cổ điển chỉ bác bỏ một tác động thật 2% khoảng 9% số lần, chỉ cao hơn một chút so với mức ý nghĩa 5% của kiểm định. Báo cáo “không có thay đổi có ý nghĩa thống kê về thanh khoản quanh thời điểm nộp hồ sơ” theo thiết kế đó về cơ bản không nói gì về việc liệu có thay đổi xảy ra hay không. Bất kỳ kết quả không ý nghĩa nào như vậy trong văn liệu này cần được đọc cùng với sức mạnh thống kê của nó.

Trần được quyết định bởi sự không đồng nhất thực, không phải bởi đo lường. Cỡ mẫu cần thiết của thước đo thực hiện chững lại quanh 35 đến 285 doanh nghiệp vì biến thiên thực giữa các doanh nghiệp trong phản ứng thanh khoản (SD_RESP = 0.10) cuối cùng chiếm ưu thế. Vượt quá một điểm nhất định, đo lường tốt hơn không còn giúp ích và chỉ có thêm doanh nghiệp mới có tác dụng.

30.7.2.4 Triển khai

Thước đo thực hiện là sự thay thế trực tiếp cho tử số ở bất cứ đâu chương này tính phi thanh khoản. Đoạn mã dưới đây cho thấy phép thay thế đối với dữ liệu bảng theo năm; các đầu vào thực hiện lấy từ cách xây dựng HOSE có nhận biết phiên được mô tả trong Mục 4.5.6, vốn đòi hỏi dữ liệu trong ngày nằm ngoài quy trình chuẩn.

Realized Amihud as the liquidity outcome
# `realized_daily` holds one row per symbol-day from the intraday pipeline:
#   [ticker, date, rv, bv, n_active, dvol_continuous]
K = np.sqrt(2 / np.pi)

rd = realized_daily.copy()
rd = rd[(rd["n_active"] >= 20) & (rd["dvol_continuous"] > 0)]     # thin-trading screen
rd["ramihud_daily"] = K * np.sqrt(rd["rv"]) / rd["dvol_continuous"]
rd["ramihud_bv_daily"] = K * np.sqrt(rd["bv"].clip(lower=0)) / rd["dvol_continuous"]

realized_annual = (
    rd.assign(fiscal_year=rd["date"].dt.year)
      .groupby(["ticker", "fiscal_year"])
      .agg(ramihud=("ramihud_daily", "mean"),
           ramihud_bv=("ramihud_bv_daily", "mean"),
           n_days=("ramihud_daily", "size"))
      .reset_index()
)
realized_annual["log_ramihud"] = np.log(realized_annual["ramihud"] + 1e-10)

# Compare against the daily-frequency measure on the overlapping sample.
compare = amihud_annual.merge(realized_annual, on=["ticker", "fiscal_year"],
                              how="inner")
print(f"Overlapping firm-years: {len(compare):,}")
print("Rank correlation between the two measures: "
      f"{compare[['log_amihud', 'log_ramihud']].corr(method='spearman').iloc[0, 1]:.3f}")

# Re-estimate the disclosure-quality regression with the realized outcome.
liq_panel_r = (
    realized_annual
    .merge(quality_panel[["ticker", "fiscal_year", "dq_index"]],
           on=["ticker", "fiscal_year"], how="inner")
    .merge(financials[["ticker", "fiscal_year", "market_cap"]],
           on=["ticker", "fiscal_year"], how="left")
    .assign(log_size=lambda d: np.log(d["market_cap"]))
    .set_index(["ticker", "fiscal_year"])
)

liq_model_r = PanelOLS(
    dependent=liq_panel_r["log_ramihud"],
    exog=sm.add_constant(liq_panel_r[["dq_index", "log_size"]]),
    entity_effects=True, time_effects=True, check_rank=False,
).fit(cov_type="clustered", cluster_entity=True)

print("Realized Amihud ~ Disclosure Quality:")
print(liq_model_r.summary)

30.7.2.5 Hướng dẫn thực hành

  1. Không chạy lại các hồi quy theo năm trên dữ liệu trong ngày. Lợi ích tối đa chỉ là vài phần trăm suy giảm, và mẫu sẽ co lại còn các doanh nghiệp thanh khoản tốt có dữ liệu trong ngày dùng được, một chi phí chọn mẫu vượt xa lợi ích đo lường.
  2. Nên dùng thước đo thực hiện cho các kiểm định cửa sổ sự kiện trong Mục 30.7, nơi nó tạo nên khác biệt giữa thiết kế khả thi và bất khả thi.
  3. Báo cáo số ngày giao dịch \(D_{i,t}\) cùng với bất kỳ thước đo phi thanh khoản đã biến đổi log nào. Đó là biến chi phối méo mó Jensen, nó tương quan với chất lượng công bố thông tin, và hầu như không bao giờ được báo cáo.
  4. Ưu tiên biến thể bipower quanh thời điểm nộp hồ sơ. Một bất ngờ lợi nhuận chính là bước nhảy thông tin làm phồng phương sai thực hiện, và ngày nộp hồ sơ là lúc nó xuất hiện. Mục 14.4 cho thấy biến thiên bipower vẫn gần như không thiên lệch khi có bước nhảy.
  5. Khi không có dữ liệu trong ngày, hãy nới rộng cửa sổ thay vì chấp nhận kết quả không ý nghĩa. Chuyển từ năm sang 21 ngày khôi phục một phần đáng kể sức mạnh bị mất do nhiễu ở tử số, với cái giá là chấp nhận thêm các sự kiện gây nhiễu, một sự đánh đổi cần được thực hiện một cách tường minh và báo cáo, chứ không phải theo mặc định.

30.7.3 Nghiên cứu sự kiện: Phản ứng thị trường đối với độ trễ nộp hồ sơ

Chúng ta xem xét liệu thị trường có phản ứng khác nhau với các doanh nghiệp nộp sớm và nộp muộn hay không bằng cách tính lợi nhuận bất thường tích lũy (CAR) quanh ngày nộp hồ sơ:

\[ CAR_{i}[\tau_1, \tau_2] = \sum_{t=\tau_1}^{\tau_2} (R_{i,t} - \hat{R}_{i,t}) \tag{30.9}\]

trong đó \(\hat{R}_{i,t}\) là lợi nhuận kỳ vọng từ một mô hình thị trường được ước lượng trên cửa sổ trước sự kiện \([-250, -30]\).

Conduct event study around filing dates by timeliness group
def compute_car(ticker, event_date, trading_df,
                est_window=(-250, -30), event_window=(-5, 10)):
    """Compute CAR around an event date using market model."""
    firm_data = trading_df[trading_df['ticker'] == ticker].copy()
    firm_data = firm_data.sort_values('date')

    # Find event date index
    event_idx = firm_data[firm_data['date'] >= event_date].index
    if len(event_idx) == 0:
        return None
    event_idx = event_idx[0]
    event_pos = firm_data.index.get_loc(event_idx)

    # Check sufficient data
    if event_pos + est_window[0] < 0:
        return None

    # Estimation window
    est_start = event_pos + est_window[0]
    est_end = event_pos + est_window[1]
    est_data = firm_data.iloc[est_start:est_end + 1]

    firm_ret = est_data['close'].pct_change()
    mkt_ret = est_data['market_return']

    valid = firm_ret.notna() & mkt_ret.notna()
    if valid.sum() < 100:
        return None

    # Market model
    X = sm.add_constant(mkt_ret[valid])
    model = sm.OLS(firm_ret[valid], X).fit()

    # Event window
    ev_start = event_pos + event_window[0]
    ev_end = event_pos + event_window[1]
    ev_data = firm_data.iloc[ev_start:ev_end + 1]

    ev_ret = ev_data['close'].pct_change()
    ev_mkt = ev_data['market_return']
    expected_ret = model.params['const'] + model.params['market_return'] * ev_mkt
    abnormal_ret = ev_ret - expected_ret

    return abnormal_ret.cumsum().values

# Sample: compute CARs for annual filings
car_results = []
for _, row in annual_filings.sample(min(2000, len(annual_filings))).iterrows():
    car = compute_car(row['ticker'], row['filing_date'], trading)
    if car is not None and len(car) == 16:  # -5 to +10
        car_results.append({
            'ticker': row['ticker'],
            'fiscal_year': row['fiscal_year'],
            'lag_tercile': row['lag_tercile'],
            'car': car
        })

car_df = pd.DataFrame(car_results)
print(f"Computed CARs for {len(car_df)} firm-year events")
Plot CAR paths by timeliness tercile
event_days = range(-5, 11)

fig, ax = plt.subplots(figsize=(10, 6))
colors = {'Early': '#27AE60', 'Middle': '#F1C40F', 'Late': '#C0392B'}

for tercile in ['Early', 'Middle', 'Late']:
    subset = car_df[car_df['lag_tercile'] == tercile]
    if len(subset) > 0:
        avg_car = np.mean(np.stack(subset['car'].values), axis=0)
        se_car = np.std(np.stack(subset['car'].values), axis=0) / np.sqrt(len(subset))
        ax.plot(event_days, avg_car, color=colors[tercile],
                linewidth=2, label=tercile)
        ax.fill_between(event_days,
                        avg_car - 1.96 * se_car,
                        avg_car + 1.96 * se_car,
                        color=colors[tercile], alpha=0.15)

ax.axvline(x=0, color='gray', linestyle='--', linewidth=0.8)
ax.axhline(y=0, color='gray', linewidth=0.5)
ax.set_xlabel('Event Day (Relative to Filing Date)')
ax.set_ylabel('Cumulative Abnormal Return')
ax.set_title('Market Reaction Around Filing Date by Timeliness')
ax.legend(title='Filing Tercile')
plt.tight_layout()
plt.show()
Hình 30.6

30.8 Tính kịp thời của việc nộp hồ sơ và chất lượng lợi nhuận

Givoly và Palmon (1982) và Chambers và Penman (1984) xác lập rằng nội dung của thông tin được công bố có tương quan với thời điểm công bố. Chúng ta kiểm định chính thức mối liên hệ này: các doanh nghiệp nộp muộn có chất lượng lợi nhuận kém hơn không?

Analyze relationship between filing lag and earnings quality
tq_panel = (
    annual_filings[['ticker', 'fiscal_year', 'lag_tercile', 'reporting_lag']]
    .merge(aq_df, on=['ticker', 'fiscal_year'], how='inner')
    .merge(persistence_df[['ticker', 'persistence']], on='ticker', how='left')
)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Panel A: Accruals quality by tercile
aq_by_tercile = tq_panel.groupby('lag_tercile')['accruals_quality'].mean()
axes[0].bar(
    range(3), aq_by_tercile.values,
    color=['#27AE60', '#F1C40F', '#C0392B'], alpha=0.85,
    edgecolor='white'
)
axes[0].set_xticks(range(3))
axes[0].set_xticklabels(['Early', 'Middle', 'Late'])
axes[0].set_ylabel('Accruals Quality (σ of DD Residuals)')
axes[0].set_title('Panel A: Accruals Quality by Filing Tercile')
axes[0].text(0.05, 0.95, 'Higher = lower quality',
             transform=axes[0].transAxes, fontsize=9,
             verticalalignment='top', style='italic', color='gray')

# Panel B: Persistence by tercile
per_by_tercile = tq_panel.groupby('lag_tercile')['persistence'].mean()
axes[1].bar(
    range(3), per_by_tercile.values,
    color=['#27AE60', '#F1C40F', '#C0392B'], alpha=0.85,
    edgecolor='white'
)
axes[1].set_xticks(range(3))
axes[1].set_xticklabels(['Early', 'Middle', 'Late'])
axes[1].set_ylabel('Earnings Persistence (AR(1) Coefficient)')
axes[1].set_title('Panel B: Earnings Persistence by Filing Tercile')

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

Chúng ta chính thức hóa điều này bằng một hồi quy có kiểm soát các đặc điểm doanh nghiệp:

Regression: accruals quality on filing lag with controls
tq_panel_reg = tq_panel.merge(
    financials[['ticker', 'fiscal_year', 'total_assets', 'net_income',
                'total_equity']],
    on=['ticker', 'fiscal_year'], how='left'
).merge(governance, on=['ticker', 'fiscal_year'], how='left')

tq_panel_reg['log_size'] = np.log(tq_panel_reg['total_assets'])
tq_panel_reg['roa'] = tq_panel_reg['net_income'] / tq_panel_reg['total_assets']
tq_panel_reg['late'] = (tq_panel_reg['lag_tercile'] == 'Late').astype(int)

model_tq = smf.ols(
    'accruals_quality ~ late + log_size + roa + state_ownership_pct '
    '+ big4_auditor + C(fiscal_year)',
    data=tq_panel_reg
).fit(cov_type='cluster', cov_kwds={'groups': tq_panel_reg['ticker']})

print("Accruals Quality ~ Late Filing:")
print(model_tq.summary().tables[1])
Lưu ý về nội sinh

Mối liên hệ giữa tính kịp thời của việc nộp hồ sơ và chất lượng lợi nhuận có khả năng mang tính nội sinh: các doanh nghiệp có vấn đề kế toán phức tạp mất nhiều thời gian hơn để chuẩn bị báo cáo tài chính, và chính sự phức tạp đó làm giảm chất lượng lợi nhuận. Vì vậy, độ trễ nộp hồ sơ được diễn giải tốt nhất như một tín hiệu quan sát được về khó khăn kế toán tiềm ẩn chứ không phải một yếu tố quyết định mang tính nhân quả. Các cách tiếp cận biến công cụ (ví dụ dùng mức độ bận rộn của kiểm toán viên trong mùa cao điểm nộp hồ sơ làm biến công cụ cho độ trễ nộp hồ sơ) có thể giải quyết một phần mối lo ngại này.

30.9 Chất lượng công bố thông tin và hiệu quả đầu tư

Biddle, Hilary, và Verdi (2009) chứng minh rằng chất lượng báo cáo tài chính cao hơn gắn với đầu tư hiệu quả hơn. Cụ thể, nó làm giảm cả đầu tư quá mức (ở các doanh nghiệp có tiền mặt dư thừa) lẫn đầu tư dưới mức (ở các doanh nghiệp bị hạn chế tài chính). Cơ chế là công bố thông tin tốt hơn làm giảm bất cân xứng thông tin giữa nhà quản lý và nhà cung cấp vốn, cải thiện việc phân bổ vốn.

Chúng ta kiểm tra dự báo này ở Việt Nam bằng khung phân tích của Biddle, Hilary, và Verdi (2009):

\[ \text{Investment}_{i,t+1} = \alpha + \beta_1 \text{SalesGrowth}_{i,t} + \varepsilon_{i,t+1} \tag{30.10}\]

Phần dư \(\hat{\varepsilon}_{i,t+1}\) đo độ lệch so với đầu tư kỳ vọng. Phần dư dương cho thấy đầu tư quá mức; phần dư âm cho thấy đầu tư dưới mức. Sau đó chúng ta kiểm tra xem giá trị tuyệt đối của phần dư này có thấp hơn đối với các doanh nghiệp có chất lượng công bố thông tin cao hơn hay không.

Estimate investment efficiency and test disclosure quality link
inv_panel = financials.sort_values(['ticker', 'fiscal_year']).copy()

# Investment = change in total assets / lagged total assets
inv_panel['investment'] = (
    inv_panel.groupby('ticker')['total_assets'].pct_change()
)
inv_panel['sales_growth'] = (
    inv_panel.groupby('ticker')['revenue'].pct_change()
)

# Expected investment model
inv_model = smf.ols(
    'investment ~ sales_growth',
    data=inv_panel
).fit()
inv_panel['inv_residual'] = inv_model.resid
inv_panel['abs_inv_residual'] = inv_panel['inv_residual'].abs()

# Merge with disclosure quality
inv_eff = (
    inv_panel[['ticker', 'fiscal_year', 'abs_inv_residual',
               'investment', 'total_assets']]
    .merge(quality_panel[['ticker', 'fiscal_year', 'dq_index']].reset_index(drop=True),
           on=['ticker', 'fiscal_year'], how='inner')
)
inv_eff['log_size'] = np.log(inv_eff['total_assets'])
inv_eff = inv_eff.set_index(['ticker', 'fiscal_year'])

# Panel regression
inv_eff_model = PanelOLS(
    dependent=inv_eff['abs_inv_residual'],
    exog=sm.add_constant(inv_eff[['dq_index', 'log_size']]),
    entity_effects=True,
    time_effects=True,
    check_rank=False
).fit(cov_type='clustered', cluster_entity=True)

print("Investment Inefficiency ~ Disclosure Quality:")
print(inv_eff_model.summary)

Hệ số âm trên dq_index cho thấy chất lượng công bố thông tin cao hơn gắn với sự kém hiệu quả đầu tư thấp hơn: các doanh nghiệp công bố tốt hơn đưa ra quyết định đầu tư gần hơn với mức mà cơ hội tăng trưởng của họ đòi hỏi.

30.10 Bối cảnh thể chế Việt Nam

30.10.1 Sở hữu nhà nước và công bố thông tin

Các doanh nghiệp nhà nước (SOE) chiếm một tỷ trọng đáng kể trong vốn hóa thị trường Việt Nam. Mối quan hệ giữa sở hữu nhà nước và chất lượng công bố thông tin là không rõ ràng về mặt lý thuyết. Một mặt, các mối quan hệ chính trị có thể làm giảm áp lực công bố minh bạch; các cổ đông nhà nước có thể chấp nhận sự thiếu minh bạch mà các cổ đông tư nhân sẽ không chấp nhận. Mặt khác, việc giám sát sau cổ phần hóa bởi nhiều bên liên quan (Bộ Tài chính, SCIC, cổ đông thiểu số) có thể tạo ra những yêu cầu công bố cạnh tranh nhau.

Compare disclosure quality between SOEs and private firms
soe_panel = (
    quality_panel[['ticker', 'fiscal_year', 'dq_index',
                    'reporting_lag']].reset_index(drop=True)
    .merge(governance[['ticker', 'fiscal_year', 'state_ownership_pct']],
           on=['ticker', 'fiscal_year'], how='inner')
)

soe_panel['soe'] = (soe_panel['state_ownership_pct'] >= 50).astype(int)
soe_panel['soe_label'] = soe_panel['soe'].map(
    {1: 'SOE (≥50%)', 0: 'Private (<50%)'}
)

# Compare means
comparison = (
    soe_panel
    .groupby('soe_label')
    .agg(
        n=('dq_index', 'count'),
        mean_dq=('dq_index', 'mean'),
        median_dq=('dq_index', 'median'),
        mean_lag=('reporting_lag', 'mean'),
        median_lag=('reporting_lag', 'median')
    )
    .round(3)
)
print("SOE vs Private Firm Disclosure Comparison:")
print(comparison)

# Formal t-test
soe_dq = soe_panel[soe_panel['soe'] == 1]['dq_index']
priv_dq = soe_panel[soe_panel['soe'] == 0]['dq_index']
t_stat, p_val = stats.ttest_ind(soe_dq.dropna(), priv_dq.dropna())
print(f"\nt-test: t = {t_stat:.3f}, p = {p_val:.4f}")

30.10.2 Hội tụ IFRS và chất lượng công bố thông tin

Việt Nam đang theo đuổi lộ trình hội tụ từng giai đoạn với IFRS, trong đó Bộ Tài chính ban hành lộ trình áp dụng tự nguyện cho các doanh nghiệp niêm yết lớn. Quá trình chuyển từ VAS sang các chuẩn mực phù hợp IFRS được kỳ vọng sẽ mở rộng yêu cầu công bố thông tin, đặc biệt đối với công cụ tài chính (IFRS 9), ghi nhận doanh thu (IFRS 15) và thuê tài sản (IFRS 16). Barth, Landsman, và Lang (2008) cung cấp bằng chứng rằng việc áp dụng IFRS gắn với cải thiện chất lượng lợi nhuận và công bố thông tin, dù tác động phụ thuộc vào mức độ thực thi.

Chúng ta có thể khai thác thời điểm áp dụng IFRS tự nguyện so le giữa các doanh nghiệp Việt Nam như một thí nghiệm tự nhiên:

Difference-in-differences: IFRS adoption and disclosure quality
# Assume DataCore provides IFRS adoption dates
ifrs_adoption = client.get_ifrs_adoption(
    exchanges=['HOSE', 'HNX'],
    fields=['ticker', 'ifrs_adoption_year']
)

# Merge with quality panel
ifrs_panel = (
    quality_panel[['ticker', 'fiscal_year', 'dq_index']].reset_index(drop=True)
    .merge(ifrs_adoption, on='ticker', how='left')
)

# Treatment indicator
ifrs_panel['post_ifrs'] = (
    ifrs_panel['fiscal_year'] >= ifrs_panel['ifrs_adoption_year']
).astype(int).fillna(0)

ifrs_panel['treated'] = ifrs_panel['ifrs_adoption_year'].notna().astype(int)

# Simple DiD
ifrs_panel = ifrs_panel.set_index(['ticker', 'fiscal_year'])
did_model = PanelOLS(
    dependent=ifrs_panel['dq_index'],
    exog=sm.add_constant(ifrs_panel[['post_ifrs']]),
    entity_effects=True,
    time_effects=True,
    check_rank=False
).fit(cov_type='clustered', cluster_entity=True)

print("DiD: IFRS Adoption and Disclosure Quality:")
print(did_model.summary)
Mối lo ngại về nhận diện

Việc áp dụng IFRS tự nguyện mang tính nội sinh vì các doanh nghiệp chọn áp dụng sớm có thể đã có chất lượng công bố thông tin cao hơn. Phương pháp DiD với hiệu ứng cố định hai chiều hấp thụ các đặc điểm doanh nghiệp không đổi theo thời gian và các xu hướng thời gian chung, nhưng không thể xử lý hoàn toàn sự chọn lọc dựa trên các yếu tố không quan sát được biến đổi theo thời gian. Các nhà nghiên cứu nên cân nhắc các công cụ ước lượng ghép cặp (ví dụ ghép điểm xu hướng theo các đặc điểm trước khi áp dụng) hoặc các cách tiếp cận biến công cụ làm kiểm tra độ vững.

30.11 Dự báo việc nộp hồ sơ muộn

Chúng ta có thể dự báo doanh nghiệp nào sẽ nộp muộn không? Điều này có giá trị cho việc xây dựng danh mục đầu tư (tránh các doanh nghiệp có thể có tin xấu) và cho các cơ quan quản lý (nhắm nguồn lực thực thi). Chúng ta dùng mô hình logistic với các biến dự báo tài chính và quản trị:

\[ \Pr(\text{Late}_{i,t} = 1) = \Lambda\left(\alpha + \boldsymbol{\beta}'\mathbf{X}_{i,t-1}\right) \tag{30.11}\]

trong đó \(\Lambda(\cdot)\) là hàm logistic và \(\mathbf{X}_{i,t-1}\) là các biến dự báo có độ trễ.

Logistic model to predict late filings
from sklearn.metrics import roc_auc_score, classification_report
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression

pred_panel = (
    annual_filings[['ticker', 'fiscal_year', 'late_filing']]
    .merge(financials, on=['ticker', 'fiscal_year'], how='left')
    .merge(governance, on=['ticker', 'fiscal_year'], how='left')
)

# Lagged predictors
pred_panel = pred_panel.sort_values(['ticker', 'fiscal_year'])
for col in ['total_assets', 'net_income', 'operating_cash_flow',
            'total_equity', 'revenue']:
    pred_panel[f'{col}_lag'] = pred_panel.groupby('ticker')[col].shift(1)

pred_panel['log_size_lag'] = np.log(pred_panel['total_assets_lag'])
pred_panel['roa_lag'] = (
    pred_panel['net_income_lag'] / pred_panel['total_assets_lag']
)
pred_panel['leverage_lag'] = (
    (pred_panel['total_assets_lag'] - pred_panel['total_equity_lag'])
    / pred_panel['total_assets_lag']
)
pred_panel['cfo_ratio_lag'] = (
    pred_panel['operating_cash_flow_lag'] / pred_panel['total_assets_lag']
)

# Previous late filing indicator
pred_panel['prev_late'] = (
    pred_panel.groupby('ticker')['late_filing'].shift(1)
)

features = [
    'log_size_lag', 'roa_lag', 'leverage_lag', 'cfo_ratio_lag',
    'state_ownership_pct', 'foreign_ownership_pct',
    'big4_auditor', 'board_independence_pct', 'prev_late'
]

clean = pred_panel.dropna(subset=features + ['late_filing'])
X = clean[features]
y = clean['late_filing']

# Standardize
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Logistic regression with cross-validation
lr = LogisticRegression(max_iter=1000, penalty='l2', C=1.0)
cv_scores = cross_val_score(lr, X_scaled, y, cv=5, scoring='roc_auc')

print(f"5-Fold Cross-Validated AUC: {cv_scores.mean():.3f} ± {cv_scores.std():.3f}")

# Fit on full sample for coefficient interpretation
lr.fit(X_scaled, y)
coef_df = pd.DataFrame({
    'Feature': features,
    'Coefficient': lr.coef_[0],
    'Odds Ratio': np.exp(lr.coef_[0])
}).sort_values('Coefficient', ascending=False)

print("\nLogistic Regression Coefficients:")
print(coef_df.to_string(index=False))
Plot ROC curve for the late-filing prediction model
from sklearn.metrics import roc_curve, auc

lr.fit(X_scaled, y)
y_prob = lr.predict_proba(X_scaled)[:, 1]
fpr, tpr, _ = roc_curve(y, y_prob)
roc_auc = auc(fpr, tpr)

fig, ax = plt.subplots(figsize=(7, 7))
ax.plot(fpr, tpr, color='#2C5F8A', linewidth=2,
        label=f'Logistic Model (AUC = {roc_auc:.3f})')
ax.plot([0, 1], [0, 1], color='gray', linestyle='--', linewidth=1)
ax.set_xlabel('False Positive Rate')
ax.set_ylabel('True Positive Rate')
ax.set_title('Late Filing Prediction: ROC Curve')
ax.legend(loc='lower right')
ax.set_aspect('equal')
plt.tight_layout()
plt.show()
Hình 30.8

30.12 Tóm tắt

Chương này đã xem xét chất lượng và thời điểm công bố thông tin doanh nghiệp ở Việt Nam theo nhiều chiều. Các phát hiện chính và đóng góp phương pháp luận được nêu trong Bảng 30.3

Bảng 30.3: Tóm tắt các phát hiện theo chủ đề.
Chủ đề Kết quả chính Tài liệu tham khảo
Tin tốt công bố sớm Các doanh nghiệp nộp sớm có CAR dương quanh ngày nộp hồ sơ Givoly và Palmon (1982)
Chất lượng văn bản Mật độ hướng tới tương lai và tính cụ thể về số liệu khác nhau đáng kể Li (2008)
Chỉ số DQ tổng hợp Sở hữu nước ngoài và công ty kiểm toán Big 4 là các yếu tố quyết định mạnh nhất Botosan (1997)
Chi phí vốn DQ cao hơn gắn với chi phí vốn cổ phần ngụ ý thấp hơn Diamond và Verrecchia (1991)
Thanh khoản Các doanh nghiệp có DQ cao hơn có phi thanh khoản Amihud thấp hơn Lang, Lins, và Maffett (2012)
Hiệu quả đầu tư DQ cao hơn làm giảm phần dư đầu tư tuyệt đối Biddle, Hilary, và Verdi (2009)
Thời điểm chiến lược Bằng chứng về việc tin xấu dồn vào những ngày ùn tắc cao Hirshleifer, Lim, và Teoh (2009)
Áp dụng IFRS Bằng chứng sơ bộ về cải thiện DQ sau khi áp dụng Barth, Landsman, và Lang (2008)

Môi trường công bố thông tin của Việt Nam được định hình bởi sự kết hợp giữa các yêu cầu bắt buộc theo quy định (Thông tư 155, Luật Chứng khoán 2019), năng lực thực thi (các chế tài của SSC và việc đình chỉ giao dịch) và các động cơ ở cấp doanh nghiệp (cấu trúc sở hữu, lựa chọn kiểm toán viên, chất lượng quản trị). Khi Việt Nam tiếp tục hội tụ IFRS và phát triển thị trường vốn, môi trường thông tin được kỳ vọng sẽ tiếp tục thay đổi, tạo cơ hội cho các nhà nghiên cứu nghiên cứu động thái của chất lượng công bố thông tin trong một bối cảnh thể chế thay đổi nhanh chóng.