44  Các mô hình cấu trúc trong tài chính

Phần lớn tài chính thực nghiệm mang tính dạng rút gọn (reduced-form): hồi quy lợi nhuận theo các đặc điểm, ước lượng phần bù rủi ro từ các danh mục nhân tố, kiểm định xem một hệ số có khác không một cách có ý nghĩa thống kê hay không. Ước lượng cấu trúc tiếp cận theo một cách về cơ bản là khác. Nó bắt đầu từ một mô hình kinh tế tường minh (tức là xác định sở thích, tập thông tin, các ràng buộc và bài toán tối ưu hóa của các tác nhân) và ước lượng trực tiếp các tham số gốc của mô hình từ dữ liệu quan sát được. Lợi ích là khả năng thực hiện phân tích phản thực tế (counterfactual): giá sẽ ra sao nếu chi phí giao dịch giảm một nửa? Mức định giá thấp khi IPO sẽ thay đổi thế nào nếu cơ chế phân bổ chuyển từ xây dựng sổ lệnh (book building) sang đấu giá đồng giá? Chi phí phúc lợi của một lựa chọn thiết kế thị trường cụ thể là bao nhiêu? Những câu hỏi này không thể trả lời trong khuôn khổ dạng rút gọn, vì chúng đòi hỏi hiểu biết về quá trình sinh dữ liệu, chứ không chỉ các mối liên hệ thống kê trong một chế độ duy nhất.

Chương này giới thiệu các khuôn khổ ước lượng cấu trúc chính được dùng trong kinh tế học tài chính và triển khai chúng cho thị trường cổ phiếu Việt Nam. Chúng ta xét bốn lĩnh vực mà mô hình cấu trúc đã tỏ ra hiệu quả nhất: ước lượng cầu của nhà đầu tư, các mô hình chi phí giao dịch và thực thi lệnh, đấu giá ở thị trường sơ cấp, và động lực của sổ lệnh giới hạn. Mỗi lĩnh vực gắn với các yếu tố kinh tế cơ bản riêng như sở thích, niềm tin, chi phí giao dịch, bất đối xứng thông tin, và mỗi lĩnh vực cần chiến lược nhận diện đặc thù.

Thị trường Việt Nam vừa đặt ra thách thức vừa mở ra cơ hội cho ước lượng cấu trúc. Về thách thức, chất lượng dữ liệu không đồng đều, vi cấu trúc thị trường đang tiến hóa, và các đặc điểm thể chế (biên độ giá, giới hạn sở hữu nước ngoài, sở hữu nhà nước) đưa vào những ràng buộc mà các mô hình chuẩn không tính đến. Về cơ hội, một số đặc điểm của thị trường Việt Nam tạo ra biến thiên tự nhiên hỗ trợ nhận diện: sự tồn tại song song của hai sàn (HOSE và HNX) với quy tắc giao dịch khác nhau, các thay đổi quy định làm dịch chuyển chi phí giao dịch và biên độ giá, và việc tự do hóa từng giai đoạn giới hạn sở hữu nước ngoài tạo ra các cú sốc cầu ngoại sinh.

Hiện mã
import pandas as pd
import numpy as np
from scipy import stats, optimize
from scipy.optimize import minimize, minimize_scalar
import statsmodels.api as sm
import statsmodels.formula.api as smf
from linearmodels.panel import PanelOLS
from linearmodels.iv import IV2SLS
import plotnine as p9
from mizani.formatters import percent_format, comma_format
import warnings
warnings.filterwarnings("ignore")

44.1 Ước lượng cấu trúc có nghĩa gì trong tài chính

44.1.1 Dạng rút gọn so với dạng cấu trúc

Xét hai nhà nghiên cứu cùng nghiên cứu tác động của chi phí giao dịch lên khối lượng giao dịch. Nhà nghiên cứu theo dạng rút gọn khai thác một sự kiện giảm phí và ước lượng:

\[ \ln V_{i,t} = \alpha + \beta \cdot \text{Post}_t + \gamma \mathbf{X}_{i,t} + \varepsilon_{i,t} \tag{44.1}\]

Hệ số \(\beta\) nhận diện tác động nhân quả của thay đổi phí lên khối lượng, nhưng nó mang tính cục bộ đối với đợt thay đổi phí này, thị trường này và giai đoạn này. Nó không cho biết một thay đổi phí khác sẽ gây ra điều gì, hay cơ cấu phí tối ưu có thể là gì.

Nhà nghiên cứu theo dạng cấu trúc viết ra một mô hình tường minh về hành vi của nhà giao dịch:

\[ \max_{q_t} \; E_t\left[\sum_{s=t}^{T} \delta^{s-t}\left(v_s q_s - c(q_s; \boldsymbol{\theta})\right)\right] \tag{44.2}\]

trong đó \(v_s\) là định giá (có thể là thông tin riêng), \(q_s\) là khối lượng giao dịch, \(c(\cdot; \boldsymbol{\theta})\) là hàm chi phí được tham số hóa bởi \(\boldsymbol{\theta}\), và \(\delta\) là hệ số chiết khấu. Nhà nghiên cứu ước lượng \(\boldsymbol{\theta}\) bằng cách yêu cầu các dự báo của mô hình khớp với các mẫu hình giao dịch quan sát được. Khi đã có \(\hat{\boldsymbol{\theta}}\), mọi hàm chi phí phản thực tế \(c'(\cdot; \boldsymbol{\theta}')\) đều có thể đưa vào mô hình để dự báo phản ứng cân bằng.

Sự khác biệt không nằm ở mức độ tinh vi (công trình dạng rút gọn cũng có thể rất chặt chẽ) mà ở loại câu hỏi được trả lời:

Bảng 44.1: Ước lượng dạng rút gọn so với dạng cấu trúc
Dạng rút gọn Dạng cấu trúc
Trả lời “Điều gì đã xảy ra?” “Điều gì sẽ xảy ra nếu…?”
Nhận diện Biến thiên ngoại sinh (sự kiện, biến công cụ) Các ràng buộc của mô hình + moment của dữ liệu
Giả định then chốt Không có biến gây nhiễu chưa quan sát (unconfoundedness) hoặc ràng buộc loại trừ Đặc tả mô hình đúng
Kết quả Hiệu ứng can thiệp, các mối liên hệ Tham số gốc, phân tích phản thực tế
Rủi ro Thiên lệch do bỏ sót biến, biến công cụ yếu Đặc tả sai mô hình

44.1.2 Nhận diện thông qua các yếu tố kinh tế cơ bản

Nhận diện cấu trúc dựa vào mô hình kinh tế để chuyển các kết quả quan sát được thành các yếu tố cơ bản không quan sát được. Ví dụ kinh điển là hệ thống cung–cầu. Chỉ quan sát giá và lượng thì không thể nhận diện các đường cầu và đường cung (vấn đề đồng thời). Nhưng nếu ta biết dạng hàm của cầu và cung, và có các biến công cụ làm dịch chuyển đường này mà không dịch chuyển đường kia, thì hệ thống được nhận diện.

Trong bối cảnh tài chính, nhận diện thường đến từ các điều kiện cân bằng của mô hình. Trong Kyle (1985), chiến lược của nhà giao dịch có thông tin, quy tắc định giá của nhà tạo lập thị trường và cầu của nhà giao dịch nhiễu cùng quyết định hệ số tác động giá cân bằng \(\lambda\). Mô hình ánh xạ đại lượng quan sát được (tức là tác động giá của dòng lệnh) sang đại lượng không quan sát được (tức là độ chính xác của thông tin riêng). Việc nhận diện đến từ cấu trúc của mô hình, chứ không phải từ một biến công cụ thông thường.

Một cách hình thức, gọi \(\boldsymbol{\theta} \in \Theta\) là vectơ các tham số cấu trúc và \(\mathbf{m}(\boldsymbol{\theta})\) là các moment do mô hình suy ra. Dữ liệu cung cấp các moment thực nghiệm \(\hat{\mathbf{m}}\). Nhận diện đòi hỏi ánh xạ \(\boldsymbol{\theta} \mapsto \mathbf{m}(\boldsymbol{\theta})\) là đơn ánh: các giá trị tham số khác nhau tạo ra các hệ quả quan sát được khác nhau.

\[ \hat{\boldsymbol{\theta}} = \arg\min_{\boldsymbol{\theta} \in \Theta} \left[\hat{\mathbf{m}} - \mathbf{m}(\boldsymbol{\theta})\right]' \mathbf{W} \left[\hat{\mathbf{m}} - \mathbf{m}(\boldsymbol{\theta})\right] \tag{44.3}\]

Đây là khuôn khổ Phương pháp moment tổng quát (GMM) áp dụng cho ước lượng cấu trúc. Việc chọn ma trận trọng số \(\mathbf{W}\) quyết định hiệu quả ước lượng, và các ràng buộc quá nhận diện (nhiều moment hơn số tham số) cung cấp các kiểm định đặc tả.

44.1.3 Đánh đổi giữa tính thực tế và tính khả giải

Mọi mô hình cấu trúc đều phải lựa chọn nên đưa vào những đặc điểm nào của thực tế và trừu tượng hóa những đặc điểm nào. Bảng 44.2 minh họa điều này cho một số mô hình tài chính kinh điển.

Bảng 44.2: Đánh đổi của các mô hình cấu trúc
Mô hình Đơn giản hóa chính Điều bị bỏ sót Điều đạt được
Kyle (1985) Một nhà giao dịch có thông tin duy nhất, phân phối chuẩn Nhiều người nội bộ, đuôi dày \(\lambda\) có dạng đóng, nhận diện rõ ràng
Glosten và Milgrom (1985) Tín hiệu nhị phân, các nhà tạo lập thị trường cạnh tranh Thông tin phức tạp, chi phí tồn kho Phân rã chênh lệch giá mua–bán
Koijen và Yogo (2019) Cầu dựa trên đặc điểm, không có tính động Tái cân bằng danh mục động Ước lượng độ co giãn của cầu trên quy mô lớn
Roll (1984) Không có thông tin, độc lập chuỗi Các giao dịch do thông tin dẫn dắt Ước lượng chênh lệch giá từ tự hiệp phương sai của lợi nhuận

Nhiệm vụ của nhà nghiên cứu là chọn mô hình đơn giản nhất nắm bắt được cơ chế kinh tế quan tâm, đồng thời vẫn đủ phong phú để các dự báo phản thực tế của nó đáng tin cậy. Như Rust (1987) nhấn mạnh, có một sự căng thẳng giữa các mô hình “đúng về cấu trúc” nhưng không thể tính toán được và các mô hình khả giải nhưng có khả năng bị đặc tả sai.

44.2 Ước lượng cầu đối với tài sản tài chính

44.2.1 Cách tiếp cận hệ thống cầu

Koijen và Yogo (2019) (sau đây gọi là KY) xây dựng một hệ thống cầu cho tài sản tài chính, ước lượng độ co giãn của cầu của nhà đầu tư tổ chức theo các đặc điểm của tài sản. Khuôn khổ này điều chỉnh phương pháp ước lượng cầu lựa chọn rời rạc của Berry, Levinsohn, và Pakes (1995) từ kinh tế học tổ chức công nghiệp sang thị trường tài chính.

Mỗi nhà đầu tư \(i\) nắm giữ một danh mục gồm \(N\) tài sản. Cầu của nhà đầu tư \(i\) đối với tài sản \(j\) tại thời điểm \(t\) là:

\[ w_{ij,t} = \frac{\exp(\delta_{j,t} + \boldsymbol{\beta}_i' \mathbf{x}_{j,t})}{1 + \sum_{k=1}^{N} \exp(\delta_{k,t} + \boldsymbol{\beta}_i' \mathbf{x}_{k,t})} \tag{44.4}\]

trong đó \(w_{ij,t}\) là tỷ trọng danh mục của tài sản \(j\) đối với nhà đầu tư \(i\), \(\delta_{j,t}\) là độ hữu dụng trung bình (định giá chung), \(\mathbf{x}_{j,t}\) là vectơ các đặc điểm quan sát được (vốn hóa thị trường, B/M, động lượng, v.v.), và \(\boldsymbol{\beta}_i\) phản ánh sở thích riêng của từng nhà đầu tư (độ co giãn cầu không đồng nhất). Mẫu số bao gồm số 1 để đại diện cho lựa chọn bên ngoài (nắm giữ tiền mặt hoặc tài sản phi cổ phiếu).

Ý tưởng then chốt là điều kiện cân bằng thị trường (market-clearing) liên kết cầu với giá cân bằng:

\[ \sum_i A_{i,t} \cdot w_{ij,t}(\boldsymbol{\theta}) = \text{ME}_{j,t} \qquad \forall j, t \tag{44.5}\]

trong đó \(A_{i,t}\) là tổng tài sản quản lý của nhà đầu tư \(i\) và \(\text{ME}_{j,t}\) là vốn hóa thị trường của tài sản \(j\). Hệ phương trình này xác định các tác động giá cân bằng và độ co giãn của cầu.

44.2.2 Ước lượng độ co giãn của cầu

Độ co giãn của cầu đối với tài sản \(j\) theo chính giá của nó (hoặc theo một đặc điểm làm dịch chuyển giá của nó) là:

\[ \varepsilon_{jj} = \frac{\partial \ln w_{ij}}{\partial \ln P_j} = \beta_{\text{price}} \cdot (1 - w_{ij}) \tag{44.6}\]

Trong một thị trường không ma sát với cầu co giãn hoàn hảo, một cú sốc cung (ví dụ: việc thêm vào chỉ số) sẽ không có tác động giá nào. Theo thực nghiệm, đường cầu đối với tài sản tài chính dốc xuống với độ co giãn hữu hạn, hàm ý rằng các cú sốc cung làm dịch chuyển giá cân bằng.

Hiện mã
# DataCore.vn API
from datacore import DataCore
dc = DataCore()

# Load institutional holdings data
holdings = dc.get_institutional_holdings(
    start_date="2012-01-01",
    end_date="2024-12-31"
)

# Load firm characteristics
firm_chars = dc.get_firm_characteristics(
    start_date="2012-01-01",
    end_date="2024-12-31"
)

# Load market data
market_data = dc.get_daily_returns(
    start_date="2012-01-01",
    end_date="2024-12-31"
)

print(f"Holdings observations: {len(holdings)}")
print(f"Unique institutions: {holdings['institution_id'].nunique()}")
print(f"Unique stocks: {holdings['ticker'].nunique()}")
Hiện mã
# Construct demand system variables
demand = holdings.merge(
    firm_chars[["ticker", "date", "market_cap", "book_to_market",
                "momentum_12m", "log_size", "profitability",
                "investment", "industry"]],
    on=["ticker", "date"],
    how="inner"
)

# Portfolio weights
demand["total_aum"] = demand.groupby(
    ["institution_id", "date"]
)["holding_value"].transform("sum")

demand["port_weight"] = demand["holding_value"] / demand["total_aum"]

# Log portfolio weight relative to outside option
# w_ij / w_i0 where w_i0 = 1 - sum(w_ij) for equity holdings
demand["sum_equity_weight"] = demand.groupby(
    ["institution_id", "date"]
)["port_weight"].transform("sum")

demand["outside_weight"] = 1 - demand["sum_equity_weight"].clip(upper=0.99)

demand["log_weight_ratio"] = np.log(
    demand["port_weight"] / demand["outside_weight"]
)
Hiện mã
# Simplified KY demand estimation
# log(w_ij / w_i0) = delta_j + beta_i' x_j + epsilon_ij
# First stage: estimate mean utility delta_j via OLS with asset FE

# Cross-sectional regression at each date
def estimate_demand_cross_section(group):
    """
    Estimate demand parameters for a single cross-section.
    Uses OLS with stock fixed effects absorbed.
    """
    g = group.dropna(subset=[
        "log_weight_ratio", "log_size", "book_to_market",
        "momentum_12m", "profitability"
    ])

    if len(g) < 50:
        return pd.Series(dtype=float)

    X = g[["log_size", "book_to_market", "momentum_12m",
           "profitability"]].values
    X = sm.add_constant(X)
    y = g["log_weight_ratio"].values

    try:
        model = sm.OLS(y, X).fit()
        return pd.Series({
            "beta_size": model.params[1],
            "beta_btm": model.params[2],
            "beta_mom": model.params[3],
            "beta_prof": model.params[4],
            "r_squared": model.rsquared,
            "n_obs": len(g)
        })
    except Exception:
        return pd.Series(dtype=float)

# Estimate by institution type
demand["institution_type"] = demand["institution_type"].fillna("Other")

demand_params = (
    demand.groupby(["institution_type", "date"])
    .apply(estimate_demand_cross_section)
    .reset_index()
    .dropna()
)
Bảng 44.3: Độ co giãn của cầu theo loại nhà đầu tư
Hiện mã
avg_params = (
    demand_params.groupby("institution_type")
    .agg(
        beta_size=("beta_size", "mean"),
        beta_btm=("beta_btm", "mean"),
        beta_mom=("beta_mom", "mean"),
        beta_prof=("beta_prof", "mean"),
        avg_r2=("r_squared", "mean"),
        n_periods=("date", "nunique")
    )
    .round(4)
)

avg_params
Hiện mã
top_types = demand_params.groupby("institution_type").size().nlargest(4).index
plot_data = demand_params[
    demand_params["institution_type"].isin(top_types)
].copy()

(
    p9.ggplot(plot_data, p9.aes(
        x="date", y="beta_size", color="institution_type"
    ))
    + p9.geom_smooth(method="lowess", se=False, size=1)
    + p9.labs(
        x="", y="β (Size)",
        title="Institutional Demand Sensitivity to Firm Size",
        color="Institution Type"
    )
    + p9.theme_minimal()
    + p9.theme(figure_size=(12, 5), legend_position="top")
)
Hình 44.1

44.2.3 Áp lực giá và tác động thị trường

Khuôn khổ hệ thống cầu trực tiếp cho ra các dự báo về tác động giá. Nếu nhà đầu tư \(i\) nhận được một dòng vốn vào ngoại sinh \(\Delta A_i\) (ví dụ: từ một cú sốc dòng vốn quỹ), giá của mỗi cổ phiếu phải điều chỉnh để cân bằng thị trường với mức cầu mới. Tác động giá cân bằng của một cú sốc cầu là:

\[ \frac{\Delta P_j}{P_j} = \frac{1}{\varepsilon_{jj}} \cdot \frac{\Delta D_j}{D_j} \tag{44.7}\]

trong đó \(\varepsilon_{jj}\) là độ co giãn của cầu và \(\Delta D_j / D_j\) là cú sốc cầu theo phần trăm. Cầu càng kém co giãn thì tác động giá càng lớn đối với cùng một cú sốc cầu (một dự báo có hàm ý trực tiếp đối với tác động giá của việc tái cân bằng chỉ số, dòng vốn quỹ và bán cưỡng bức).

Hiện mã
# Estimate aggregate demand elasticity via index rebalancing events
# Use VN30 index reconstitutions as demand shocks

index_changes = dc.get_index_changes(
    index="VN30",
    start_date="2012-01-01",
    end_date="2024-12-31"
)

# Event study: abnormal returns around index additions/deletions
def event_study_car(events, returns, window=(-5, 20)):
    """
    Compute cumulative abnormal returns around events.

    Parameters
    ----------
    events : DataFrame
        Columns: ticker, event_date, event_type (addition/deletion).
    returns : DataFrame
        Columns: ticker, date, ret, mkt_ret.

    Returns
    -------
    DataFrame : CARs by event type and event day.
    """
    results = []

    for _, event in events.iterrows():
        ticker = event["ticker"]
        event_date = pd.to_datetime(event["event_date"])

        # Estimation window: -260 to -11
        stock_rets = returns[returns["ticker"] == ticker].copy()
        stock_rets = stock_rets.sort_values("date")

        est_mask = (
            (stock_rets["date"] >= event_date - pd.Timedelta(days=365)) &
            (stock_rets["date"] < event_date - pd.Timedelta(days=15))
        )
        est_data = stock_rets[est_mask].dropna(subset=["ret", "mkt_ret"])

        if len(est_data) < 60:
            continue

        # Market model
        model = sm.OLS(
            est_data["ret"],
            sm.add_constant(est_data["mkt_ret"])
        ).fit()

        # Event window
        evt_mask = (
            (stock_rets["date"] >= event_date + pd.Timedelta(days=window[0] * 1.5)) &
            (stock_rets["date"] <= event_date + pd.Timedelta(days=window[1] * 1.5))
        )
        evt_data = stock_rets[evt_mask].dropna(subset=["ret", "mkt_ret"])

        if len(evt_data) < 5:
            continue

        # Abnormal returns
        evt_data = evt_data.copy()
        evt_data["expected"] = model.predict(
            sm.add_constant(evt_data["mkt_ret"])
        )
        evt_data["ar"] = evt_data["ret"] - evt_data["expected"]

        # Assign event-time index
        trading_dates = evt_data["date"].sort_values().values
        event_idx = np.searchsorted(trading_dates, event_date)
        evt_data["event_day"] = range(-event_idx, len(evt_data) - event_idx)

        evt_data["event_type"] = event["event_type"]
        evt_data["ticker"] = ticker
        results.append(evt_data[["ticker", "event_day", "ar", "event_type"]])

    if not results:
        return pd.DataFrame()

    return pd.concat(results, ignore_index=True)

# Compute event study
daily_data = market_data.merge(
    dc.get_market_returns(
        start_date="2012-01-01",
        end_date="2024-12-31",
        frequency="daily"
    )[["date", "mkt_ret"]],
    on="date", how="left"
)

car_results = event_study_car(index_changes, daily_data)
Hiện mã
if len(car_results) > 0:
    # Average AR by event day and type
    avg_ar = (
        car_results.groupby(["event_type", "event_day"])
        .agg(
            mean_ar=("ar", "mean"),
            se_ar=("ar", lambda x: x.std() / np.sqrt(len(x))),
            n=("ar", "count")
        )
        .reset_index()
    )

    # Cumulative AR
    for etype in avg_ar["event_type"].unique():
        mask = avg_ar["event_type"] == etype
        avg_ar.loc[mask, "car"] = avg_ar.loc[mask, "mean_ar"].cumsum()

    avg_ar = avg_ar[avg_ar["event_day"].between(-5, 20)]

    (
        p9.ggplot(avg_ar, p9.aes(
            x="event_day", y="car", color="event_type"
        ))
        + p9.geom_line(size=1)
        + p9.geom_vline(xintercept=0, linetype="dashed", color="gray")
        + p9.geom_hline(yintercept=0, linetype="dotted", color="gray")
        + p9.scale_color_manual(values=["#27AE60", "#C0392B"])
        + p9.labs(
            x="Event Day",
            y="Cumulative Abnormal Return",
            title="Price Pressure from VN30 Index Additions and Deletions",
            color="Event"
        )
        + p9.theme_minimal()
        + p9.theme(figure_size=(10, 6))
    )
Hình 44.2

Thành phần vĩnh viễn của CAR đo hàm lượng thông tin của việc đưa vào chỉ số, trong khi thành phần tạm thời (đảo chiều sau sự kiện) đo áp lực giá thuần túy từ cầu. Trong một thế giới có cầu co giãn hoàn hảo, sẽ không có tác động giá tạm thời. Độ lớn của thành phần tạm thời tỷ lệ nghịch với độ co giãn của cầu, cung cấp chiến lược nhận diện thứ hai (bổ sung cho cách tiếp cận dựa trên vị thế nắm giữ) đối với các đường cầu.

44.2.4 Cầu kém co giãn và các hàm ý

Gabaix và Koijen (2021) lập luận rằng tổng cầu đối với cổ phiếu kém co giãn một cách đáng kể, với các ước lượng độ co giãn vào khoảng 0,2 (nghĩa là nguồn cung tăng 1% đòi hỏi giá giảm 5% để cân bằng thị trường). Hàm ý là rất sâu sắc: nếu cầu kém co giãn đến vậy, thì các dòng vốn (từ quỹ chỉ số, nhà đầu tư nước ngoài, nhà đầu tư cá nhân) có tác động lên giá lớn không tương xứng. Ở thị trường Việt Nam, nơi giới hạn sở hữu nước ngoài tạo ra các ràng buộc có hiệu lực đối với một nhóm nhà đầu tư then chốt, mức độ kém co giãn của cầu có thể còn nghiêm trọng hơn.

Sự kém co giãn này cũng tạo ra vai trò cho “danh mục thị trường” như một khái niệm cân bằng: nếu hầu hết nhà đầu tư nắm giữ danh mục gần với thị trường (như trong CAPM), thì các độ lệch khỏi tỷ trọng thị trường đòi hỏi ai đó phải hấp thụ phần cung dư, và khoản bù đắp cần thiết là nghịch đảo của độ co giãn của cầu.

44.3 Các mô hình cấu trúc về chiến lược giao dịch

44.3.1 Thực thi tối ưu: Khuôn khổ Almgren-Chriss

Bài toán thực thi tối ưu, được Almgren và Chriss (2001) hình thức hóa, đặt câu hỏi: với một lệnh lớn cần thực thi trong một khoảng thời gian cố định, lịch giao dịch tối ưu nào sẽ tối thiểu hóa tổng chi phí thực thi? Bài toán này mang tính nền tảng đối với đầu tư tổ chức vì các lệnh lớn không thể được thực thi tức thời mà không gây ra tác động giá nghiêm trọng.

Gọi \(X_t\) là số cổ phiếu còn lại cần bán tại thời điểm \(t\), với \(X_0 = X\) (tổng lệnh) và \(X_T = 0\) (hoàn tất). Tốc độ giao dịch là \(n_t = X_{t-1} - X_t\). Giá thực thi của số cổ phiếu giao dịch tại \(t\) chịu ảnh hưởng của cả tác động giá tạm thời và vĩnh viễn:

\[ S_t = S_0 + \sigma W_t - g(n_t) - h\left(\frac{n_t}{\tau}\right) \tag{44.8}\]

trong đó \(g(\cdot)\) là hàm tác động vĩnh viễn, \(h(\cdot)\) là hàm tác động tạm thời, \(\sigma\) là độ biến động, \(W_t\) là quá trình Wiener, và \(\tau\) là khoảng thời gian. Tổng mức thiếu hụt thực thi (implementation shortfall, chi phí thực thi so với giá tại thời điểm lệnh đến \(S_0\)) là:

\[ \text{IS} = \sum_{t=1}^{T} n_t (S_0 - S_t) = \sum_{t=1}^{T} n_t \left[\sigma W_t + g(n_t) + h\left(\frac{n_t}{\tau}\right)\right] \tag{44.9}\]

Nhà giao dịch tối thiểu hóa một hàm mục tiêu trung bình–phương sai:

\[ \min_{\{n_t\}} \; E[\text{IS}] + \lambda \cdot \text{Var}(\text{IS}) \tag{44.10}\]

trong đó \(\lambda\) là tham số ác cảm rủi ro. Với các hàm tác động tuyến tính \(g(n) = \gamma n\) và \(h(v) = \eta v + \epsilon \text{sgn}(v)\), chiến lược tối ưu có nghiệm dạng đóng.

TWAP (Time-Weighted Average Price, giá trung bình theo thời gian): Giao dịch đều đặn: \(n_t = X / T\) với mọi \(t\). Tối ưu khi tác động vĩnh viễn trội hơn tác động tạm thời.

VWAP (Volume-Weighted Average Price, giá trung bình theo khối lượng): Giao dịch tỷ lệ với khối lượng kỳ vọng: \(n_t \propto \hat{V}_t\). Xấp xỉ TWAP có điều chỉnh theo mẫu hình khối lượng trong ngày.

Tối ưu Almgren-Chriss: Quỹ đạo tối ưu cho nhà giao dịch ác cảm rủi ro với tác động tuyến tính là:

\[ x_t^* = X \cdot \frac{\sinh(\kappa (T - t))}{\sinh(\kappa T)} \tag{44.11}\]

trong đó \(\kappa = \sqrt{\lambda \sigma^2 / \eta}\) chi phối sự đánh đổi giữa tính cấp bách (giao dịch nhanh để giảm rủi ro) và sự kiên nhẫn (giao dịch chậm để giảm tác động). Mức ác cảm rủi ro cao (\(\lambda\) lớn) hàm ý thực thi dồn về phía đầu.

Hiện mã
def almgren_chriss_trajectory(X, T, sigma, eta, gamma, lam, n_steps=100):
    """
    Compute Almgren-Chriss optimal execution trajectory.

    Parameters
    ----------
    X : float
        Total shares to execute.
    T : float
        Execution horizon (in trading days).
    sigma : float
        Daily return volatility.
    eta : float
        Temporary impact parameter.
    gamma : float
        Permanent impact parameter.
    lam : float
        Risk aversion parameter.
    n_steps : int
        Number of time steps.

    Returns
    -------
    DataFrame : Time, remaining shares, trading rate, expected cost.
    """
    tau = T / n_steps
    kappa_sq = lam * sigma**2 / (eta / tau)

    if kappa_sq <= 0:
        # Risk-neutral: trade uniformly
        kappa = 0
        x_t = np.linspace(X, 0, n_steps + 1)
    else:
        kappa = np.sqrt(kappa_sq)
        t_grid = np.linspace(0, T, n_steps + 1)
        x_t = X * np.sinh(kappa * (T - t_grid)) / np.sinh(kappa * T)

    # Trading rates
    n_t = -np.diff(x_t)

    # Expected cost components
    perm_cost = gamma * np.sum(n_t**2)
    temp_cost = (eta / tau) * np.sum(n_t**2)
    total_expected_cost = perm_cost + temp_cost

    # Variance of cost
    var_cost = sigma**2 * tau * np.sum(x_t[:-1]**2)

    results = pd.DataFrame({
        "time": np.linspace(0, T, n_steps + 1)[:-1],
        "remaining_shares": x_t[:-1],
        "trade_rate": n_t
    })

    results.attrs["expected_cost"] = total_expected_cost
    results.attrs["cost_variance"] = var_cost
    results.attrs["kappa"] = kappa if kappa_sq > 0 else 0

    return results
Hiện mã
# Estimate market impact parameters from Vietnamese data
# Typical values for mid-cap Vietnamese stocks
sigma_daily = 0.025  # 2.5% daily vol
eta = 2.5e-7         # Temporary impact
gamma = 1.0e-7       # Permanent impact
X_shares = 500000    # 500k shares to sell
T_days = 5           # 5-day horizon

trajectories = []
for lam, label in [(0, "Risk Neutral (TWAP)"),
                    (1e-6, "Low Risk Aversion"),
                    (1e-5, "Medium Risk Aversion"),
                    (1e-4, "High Risk Aversion")]:
    traj = almgren_chriss_trajectory(
        X_shares, T_days, sigma_daily, eta, gamma, lam
    )
    traj["strategy"] = label
    traj["pct_remaining"] = traj["remaining_shares"] / X_shares * 100
    trajectories.append(traj)

traj_all = pd.concat(trajectories, ignore_index=True)

(
    p9.ggplot(traj_all, p9.aes(
        x="time", y="pct_remaining", color="strategy"
    ))
    + p9.geom_line(size=1)
    + p9.scale_color_manual(
        values=["#95A5A6", "#27AE60", "#2E5090", "#C0392B"]
    )
    + p9.labs(
        x="Time (Trading Days)",
        y="Remaining Order (%)",
        title="Almgren-Chriss Optimal Execution Trajectories",
        color="Strategy"
    )
    + p9.theme_minimal()
    + p9.theme(figure_size=(10, 6), legend_position="top")
)
Hình 44.3

44.3.2 Ước lượng tác động thị trường từ dữ liệu giao dịch

Các tham số cấu trúc \((\gamma, \eta, \sigma)\) phải được ước lượng từ dữ liệu. Cách tiếp cận chuẩn sử dụng quy luật căn bậc hai của tác động thị trường, được Kyle (1985) và Hasbrouck (1991) ghi nhận bằng thực nghiệm và được Gabaix và c.s. (2003) suy ra về mặt lý thuyết:

\[ \Delta P / P = c \cdot \text{sgn}(Q) \cdot |Q / V|^{\delta} \tag{44.12}\]

trong đó \(Q\) là dòng lệnh có dấu, \(V\) là khối lượng hằng ngày, \(c\) là hệ số tác động, và \(\delta \approx 0.5\) là số mũ tác động. “Căn bậc hai” ám chỉ \(\delta = 0.5\), một giá trị bền vững đáng kể giữa các thị trường, giai đoạn và loại tài sản.

Hiện mã
# Load intraday transaction data (or daily order flow proxy)
order_flow = dc.get_order_flow(
    start_date="2018-01-01",
    end_date="2024-12-31",
    frequency="daily"
)

# Construct signed order flow using Lee-Ready classification
# (buy-initiated minus sell-initiated volume)
impact_data = order_flow.merge(
    dc.get_daily_returns(
        start_date="2018-01-01",
        end_date="2024-12-31"
    )[["ticker", "date", "ret", "volume", "market_cap"]],
    on=["ticker", "date"],
    how="inner"
)

# Normalize order flow
impact_data["oib"] = impact_data["net_buy_volume"] / impact_data["volume"]
impact_data["abs_oib"] = np.abs(impact_data["oib"])
impact_data["sign_oib"] = np.sign(impact_data["oib"])

# Log-log regression: ln|ΔP| = α + δ ln|Q/V| + ε
impact_data["ln_abs_ret"] = np.log(np.abs(impact_data["ret"]).clip(lower=1e-8))
impact_data["ln_abs_oib"] = np.log(impact_data["abs_oib"].clip(lower=1e-8))

# Estimate by size quintile
impact_data["size_quintile"] = impact_data.groupby("date")[
    "market_cap"
].transform(lambda x: pd.qcut(x, 5, labels=[1, 2, 3, 4, 5],
                                duplicates="drop"))

impact_results = []
for q in range(1, 6):
    subset = impact_data[impact_data["size_quintile"] == q].dropna(
        subset=["ln_abs_ret", "ln_abs_oib"]
    )
    if len(subset) < 500:
        continue

    model = sm.OLS(
        subset["ln_abs_ret"],
        sm.add_constant(subset["ln_abs_oib"])
    ).fit(cov_type="HC1")

    impact_results.append({
        "size_quintile": q,
        "delta_hat": model.params.iloc[1],
        "delta_se": model.bse.iloc[1],
        "intercept": model.params.iloc[0],
        "r_squared": model.rsquared,
        "n_obs": int(model.nobs)
    })

impact_df = pd.DataFrame(impact_results)
Bảng 44.4: Số mũ tác động thị trường theo nhóm ngũ phân vị quy mô doanh nghiệp
Hiện mã
impact_df.round(4)

Số mũ tác động \(\hat{\delta}\) gần 0.5 ở mọi nhóm ngũ phân vị quy mô xác nhận tính phổ quát của quy luật căn bậc hai. Các doanh nghiệp nhỏ hơn thường có hệ số tác động cao hơn (\(c\) lớn hơn), phù hợp với thanh khoản thấp hơn.

44.3.3 Phân rã chi phí giao dịch

Tổng chi phí giao dịch gồm nhiều thành phần, mỗi thành phần có nội dung kinh tế riêng:

\[ \text{TC} = \underbrace{\frac{s}{2}}_{\text{Half-spread}} + \underbrace{\gamma \cdot Q}_{\text{Permanent impact}} + \underbrace{\eta \cdot \frac{Q}{V}}_{\text{Temporary impact}} + \underbrace{\sigma \sqrt{T}}_{\text{Timing risk}} \tag{44.13}\]

Chúng ta ước lượng riêng từng thành phần, theo phương pháp của Hasbrouck (2009) để phân rã chênh lệch giá hiệu dụng.

Hiện mã
# Effective spread estimation
spreads = dc.get_bid_ask_data(
    start_date="2020-01-01",
    end_date="2024-12-31"
)

# Quoted spread
spreads["quoted_spread"] = (
    (spreads["ask"] - spreads["bid"]) / spreads["midpoint"]
)

# Effective spread (from transaction prices)
spreads["effective_spread"] = (
    2 * np.abs(spreads["trade_price"] - spreads["midpoint"]) /
    spreads["midpoint"]
)

# Roll (1984) implied spread from return autocovariance
def roll_spread(returns):
    """
    Estimate the Roll (1984) bid-ask spread.
    Spread = 2 * sqrt(-Cov(r_t, r_{t-1})) if covariance is negative.
    """
    cov = np.cov(returns[1:], returns[:-1])[0, 1]
    if cov < 0:
        return 2 * np.sqrt(-cov)
    else:
        return np.nan

# Compute by stock-month
daily_rets = dc.get_daily_returns(
    start_date="2020-01-01",
    end_date="2024-12-31"
)
daily_rets["month"] = daily_rets["date"].dt.to_period("M")

roll_spreads = (
    daily_rets.groupby(["ticker", "month"])
    .agg(
        roll_spread=("ret", lambda x: roll_spread(x.values)
                     if len(x) > 10 else np.nan),
        n_days=("ret", "count"),
        avg_volume=("volume", "mean"),
        market_cap=("market_cap", "last")
    )
    .reset_index()
    .dropna(subset=["roll_spread"])
)
Hiện mã
# Bin by market cap deciles
roll_spreads["size_decile"] = roll_spreads.groupby("month")[
    "market_cap"
].transform(lambda x: pd.qcut(x, 10, labels=range(1, 11),
                                duplicates="drop"))

spread_by_size = (
    roll_spreads.groupby("size_decile")
    .agg(
        median_spread=("roll_spread", "median"),
        mean_spread=("roll_spread", "mean"),
        q25=("roll_spread", lambda x: x.quantile(0.25)),
        q75=("roll_spread", lambda x: x.quantile(0.75))
    )
    .reset_index()
)

(
    p9.ggplot(spread_by_size, p9.aes(x="size_decile", y="median_spread"))
    + p9.geom_bar(stat="identity", fill="#2E5090", alpha=0.7)
    + p9.geom_errorbar(
        p9.aes(ymin="q25", ymax="q75"),
        width=0.3, color="#2E5090"
    )
    + p9.labs(
        x="Size Decile (1 = Smallest)",
        y="Roll Implied Spread",
        title="Transaction Costs Decrease Monotonically with Firm Size"
    )
    + p9.scale_y_continuous(labels=percent_format())
    + p9.theme_minimal()
    + p9.theme(figure_size=(10, 5))
)
Hình 44.4

44.4 Các mô hình đấu giá ở thị trường sơ cấp

44.4.1 Bài toán phân bổ cổ phiếu IPO

Phát hành cổ phiếu lần đầu ra công chúng (IPO) đặt ra một bài toán bất đối xứng thông tin kinh điển. Tổ chức phát hành và tổ chức bảo lãnh không biết giá cân bằng thị trường thực sự; các nhà đầu tư có thông tin thì biết (xấp xỉ). Cơ chế phân bổ (tức là cổ phiếu được phân phối ra sao và ở mức giá nào) quyết định hiệu quả định giá của đợt IPO, mức độ định giá thấp, và việc phân chia thặng dư giữa tổ chức phát hành và nhà đầu tư.

Rock (1986) đưa ra mô hình kinh điển. Có hai loại nhà đầu tư: có thông tin (biết giá trị thực \(v\)) và không có thông tin (chỉ biết phân phối \(v \sim F\)). Nếu IPO được định giá ở mức \(P\):

  • Khi \(v > P\) (IPO tốt): cả nhà đầu tư có thông tin và không có thông tin đều đăng ký mua, do đó nhà đầu tư không có thông tin chỉ nhận được một phần lệnh của mình (phân bổ theo tỷ lệ).
  • Khi \(v < P\) (IPO xấu): chỉ nhà đầu tư không có thông tin đăng ký mua, do đó họ được phân bổ đầy đủ (“lời nguyền người thắng cuộc”).

Lợi nhuận kỳ vọng của nhà đầu tư không có thông tin, có tính đến việc phân bổ theo tỷ lệ, là:

\[ E[R_{\text{uninformed}}] = \alpha_g \cdot E\left[\frac{v - P}{P} \mid v > P\right] \cdot \Pr(v > P) + E\left[\frac{v - P}{P} \mid v \leq P\right] \cdot \Pr(v \leq P) \tag{44.14}\]

trong đó \(\alpha_g < 1\) là xác suất được phân bổ trong các IPO tốt (bị phân bổ theo tỷ lệ) và mức phân bổ bằng 1 trong các IPO xấu. Để nhà đầu tư không có thông tin tham gia, cần \(E[R_{\text{uninformed}}] \geq 0\), điều này đòi hỏi:

\[ E\left[\frac{v - P}{P}\right] > 0 \tag{44.15}\]

Nghĩa là, các đợt IPO phải được định giá thấp trung bình để bù đắp cho nhà đầu tư không có thông tin trước lời nguyền người thắng cuộc. Mức định giá thấp cần thiết tăng theo tỷ lệ nhà đầu tư có thông tin và phương sai của giá trị thực.

44.4.2 Xây dựng sổ lệnh so với cơ chế đấu giá

Lịch sử IPO của Việt Nam cung cấp biến thiên về cơ chế phân bổ. Các đợt cổ phần hóa doanh nghiệp nhà nước đã dùng đấu giá kiểu Hà Lan, trong khi các đợt IPO của khu vực tư nhân dùng phương pháp xây dựng sổ lệnh. Sự khác biệt về thể chế này cho phép so sánh cấu trúc giữa các cơ chế.

Xây dựng sổ lệnh (Book Building) (Benveniste và Spindt 1989): Tổ chức bảo lãnh thu thập các biểu thị quan tâm (indications of interest) từ nhà đầu tư tổ chức trong quá trình roadshow. Những nhà đầu tư tiết lộ thông tin tích cực (định giá cao) được phân bổ ưu đãi như một khoản đền bù. Cơ chế này tổng hợp thông tin hiệu quả nhưng trao quyền tùy nghi cho tổ chức bảo lãnh.

Đấu giá đồng giá (Uniform-Price Auction): Mọi nhà đầu tư đặt giá trúng đều trả cùng một mức giá cân bằng thị trường. Cơ chế này loại bỏ quyền tùy nghi phân bổ của tổ chức bảo lãnh nhưng có thể dẫn đến hành vi ăn theo (free-riding) trong việc tiết lộ thông tin.

Hiện mã
# Load IPO data
ipo_data = dc.get_ipo_data(
    start_date="2005-01-01",
    end_date="2024-12-31"
)

# Compute first-day returns (underpricing)
ipo_data["underpricing"] = (
    (ipo_data["first_day_close"] - ipo_data["offer_price"]) /
    ipo_data["offer_price"]
)

# Classify mechanism
ipo_data["mechanism"] = ipo_data["ipo_method"].map({
    "auction": "Auction",
    "book_building": "Book Building",
    "fixed_price": "Fixed Price"
})

print(f"Total IPOs: {len(ipo_data)}")
print(f"By mechanism:\n{ipo_data['mechanism'].value_counts()}")
Bảng 44.5: Mức định giá thấp khi IPO theo cơ chế phân bổ
Hiện mã
ipo_summary = (
    ipo_data.groupby("mechanism")
    .agg(
        n_ipos=("underpricing", "count"),
        mean_underpricing=("underpricing", "mean"),
        median_underpricing=("underpricing", "median"),
        std_underpricing=("underpricing", "std"),
        pct_positive=("underpricing", lambda x: (x > 0).mean()),
        mean_proceeds=("proceeds_bn_vnd", "mean")
    )
    .round(4)
)
ipo_summary

44.4.3 Ước lượng cấu trúc về bất đối xứng thông tin

Chúng ta ước lượng các tham số của mô hình Rock (1986) (tức là tỷ lệ nhà đầu tư có thông tin (\(\mu\)) và độ chính xác của thông tin của họ (\(\sigma_v^2\))) bằng phương pháp moment mô phỏng (MSM).

Mô hình dự báo hai moment chính:

1 . Mức định giá thấp trung bình: \(E[U] = f(\mu, \sigma_v^2)\) 2. Phương sai theo mặt cắt ngang của mức định giá thấp: \(\text{Var}(U) = g(\mu, \sigma_v^2)\)

Chúng ta khớp các moment do mô hình suy ra này với dữ liệu.

Hiện mã
def rock_model_moments(params, n_sim=10000):
    """
    Simulate Rock (1986) IPO model and compute moments.

    Parameters
    ----------
    params : tuple
        (mu, sigma_v) - fraction of informed investors,
        std dev of true value.

    Returns
    -------
    tuple : (mean underpricing, variance of underpricing).
    """
    mu, sigma_v = params
    np.random.seed(42)

    # True values
    v = np.random.lognormal(mean=0, sigma=sigma_v, size=n_sim)

    # Offer price: set to break even for uninformed
    # Simplified: P = E[v] * discount_factor
    P = np.exp(sigma_v**2 / 2) * 0.9  # 10% average discount

    # First-day returns
    returns = (v - P) / P

    # Allocation probability for uninformed in good IPOs
    # Depends on mu: more informed -> more rationing
    good_mask = v > P
    alpha_g = (1 - mu) / 1.0  # Simplified rationing

    # Uninformed realized returns
    uninformed_returns = np.where(
        good_mask,
        alpha_g * returns,
        returns
    )

    mean_u = uninformed_returns.mean()
    var_u = uninformed_returns.var()

    return mean_u, var_u


def rock_model_objective(params, target_moments, weight_matrix=None):
    """
    GMM objective for Rock model estimation.
    """
    mu, sigma_v = params

    if mu <= 0 or mu >= 1 or sigma_v <= 0 or sigma_v > 2:
        return 1e10

    model_moments = rock_model_moments(params)
    diff = np.array(model_moments) - np.array(target_moments)

    if weight_matrix is None:
        weight_matrix = np.eye(len(diff))

    return diff @ weight_matrix @ diff


# Target moments from data
target_mean = ipo_data["underpricing"].mean()
target_var = ipo_data["underpricing"].var()

# Estimate
result = minimize(
    rock_model_objective,
    x0=[0.3, 0.5],
    args=([target_mean, target_var],),
    method="Nelder-Mead",
    options={"maxiter": 5000}
)

mu_hat, sigma_v_hat = result.x
print(f"Rock Model Estimates:")
print(f"  Fraction informed (μ): {mu_hat:.4f}")
print(f"  Value uncertainty (σ_v): {sigma_v_hat:.4f}")
print(f"  Objective value: {result.fun:.6f}")
Hiện mã
ipo_plot = ipo_data.dropna(subset=["underpricing", "mechanism"]).copy()
ipo_plot["year"] = pd.to_datetime(ipo_plot["ipo_date"]).dt.year

annual_underpricing = (
    ipo_plot.groupby(["year", "mechanism"])
    .agg(
        mean_up=("underpricing", "mean"),
        n=("underpricing", "count")
    )
    .reset_index()
    .query("n >= 3")
)

(
    p9.ggplot(annual_underpricing, p9.aes(
        x="year", y="mean_up", color="mechanism"
    ))
    + p9.geom_line(size=1)
    + p9.geom_point(p9.aes(size="n"))
    + p9.geom_hline(yintercept=0, linetype="dashed", color="gray")
    + p9.scale_color_manual(values=["#2E5090", "#C0392B", "#27AE60"])
    + p9.scale_y_continuous(labels=percent_format())
    + p9.labs(
        x="Year",
        y="Average First-Day Return",
        title="IPO Underpricing by Allocation Mechanism",
        color="Mechanism", size="# IPOs"
    )
    + p9.theme_minimal()
    + p9.theme(figure_size=(12, 6))
)
Hình 44.5

44.4.4 Phân tích phản thực tế: Phúc lợi dưới các cơ chế thay thế

Với các tham số cấu trúc \((\hat{\mu}, \hat{\sigma}_v)\) đã có, chúng ta có thể mô phỏng các kết quả phản thực tế. Chẳng hạn: nếu mọi đợt IPO ở Việt Nam đều dùng đấu giá đồng giá thay vì xây dựng sổ lệnh, mức định giá thấp và phúc lợi sẽ thay đổi ra sao?

Hiện mã
def simulate_ipo_mechanism(mu, sigma_v, mechanism="book_building",
                           n_sim=50000):
    """
    Simulate IPO outcomes under different mechanisms.

    Returns issuer surplus, informed profit, uninformed profit.
    """
    np.random.seed(42)
    v = np.random.lognormal(mean=0, sigma=sigma_v, size=n_sim)

    if mechanism == "book_building":
        # Price partially reveals information
        # P = E[v] + 0.5 * (v - E[v]) * signal_quality
        signal = v + np.random.normal(0, sigma_v * 0.5, n_sim)
        P = np.exp(sigma_v**2 / 2) + 0.3 * (signal - np.exp(sigma_v**2 / 2))
        P = P.clip(min=0.1)

    elif mechanism == "auction":
        # Competitive bidding: P closer to v
        noise = np.random.normal(0, sigma_v * 0.3, n_sim)
        P = v + noise
        P = P.clip(min=0.1)

    elif mechanism == "fixed_price":
        # Fixed at E[v] * discount
        P = np.full(n_sim, np.exp(sigma_v**2 / 2) * 0.85)

    underpricing = (v - P) / P

    issuer_surplus = P  # Revenue per share
    investor_surplus = v - P  # Profit per share

    return {
        "mechanism": mechanism,
        "mean_underpricing": underpricing.mean(),
        "median_underpricing": np.median(underpricing),
        "std_underpricing": underpricing.std(),
        "issuer_revenue": issuer_surplus.mean(),
        "investor_profit": investor_surplus.mean(),
        "money_left": (investor_surplus[investor_surplus > 0]).sum() / n_sim
    }

# Compare mechanisms
counterfactual = pd.DataFrame([
    simulate_ipo_mechanism(mu_hat, sigma_v_hat, "book_building"),
    simulate_ipo_mechanism(mu_hat, sigma_v_hat, "auction"),
    simulate_ipo_mechanism(mu_hat, sigma_v_hat, "fixed_price")
]).set_index("mechanism").round(4)

counterfactual

44.5 Các mô hình sổ lệnh giới hạn

44.5.1 Việc đặt lệnh như một lựa chọn chiến lược

Trong thị trường lệnh giới hạn, mọi nhà giao dịch đều đối mặt với một đánh đổi cơ bản: đặt lệnh thị trường (khớp ngay, nhưng ở mức giá bất lợi) hay lệnh giới hạn (giá tốt hơn, nhưng có rủi ro không được khớp). Parlour (1998) mô hình hóa điều này như một trò chơi tuần tự, trong đó chiến lược tối ưu của mỗi nhà giao dịch phụ thuộc vào trạng thái hiện tại của sổ lệnh.

Gọi \(a_t\) và \(b_t\) là giá bán tốt nhất và giá mua tốt nhất tại thời điểm \(t\), với chênh lệch giá \(s_t = a_t - b_t\). Một người mua đến tại thời điểm \(t\) lựa chọn giữa:

  • Mua theo lệnh thị trường: Khớp ngay tại \(a_t\). Chi phí: \(a_t - v_t\) với \(v_t\) là giá trị thực.
  • Mua theo lệnh giới hạn tại \(b_t\): Cung cấp thanh khoản. Nếu được khớp, lợi nhuận: \(v_t - b_t\). Xác suất được khớp: \(\pi(b_t, \text{book state})\).

Người mua đặt lệnh thị trường nếu:

\[ a_t - v_t < (1 - \pi_t)(v_t - b_t) + \pi_t \cdot 0 \tag{44.16}\]

Sắp xếp lại: lệnh thị trường là tối ưu khi chênh lệch giá hẹp so với rủi ro không được khớp của lệnh giới hạn. Điều này tạo ra mẫu hình quan sát được trong thực nghiệm rằng lệnh giới hạn hấp dẫn hơn khi chênh lệch giá rộng và sổ lệnh mỏng.

44.5.2 Mô hình Glosten-Milgrom

Glosten và Milgrom (1985) đưa ra mô hình cấu trúc nền tảng về việc xác định chênh lệch giá mua–bán trong điều kiện thông tin bất đối xứng. Một nhà tạo lập thị trường cạnh tranh đặt giá mua và giá bán sao cho hòa vốn theo kỳ vọng, nhận thức rằng một số giao dịch đến từ các nhà giao dịch có thông tin.

Gọi \(\mu\) là xác suất một lệnh đến là từ nhà giao dịch có thông tin, và gọi \(V^H\) và \(V^L\) là giá trị cao và thấp của tài sản (\(\Pr(V = V^H) = p\)). Các điều kiện lợi nhuận bằng không cho ra:

\[ a = E[V \mid \text{buy order}] = \frac{p(1-\mu) + p\mu}{(1-\mu) + p\mu} V^H + \frac{(1-p)(1-\mu)}{(1-\mu) + p\mu} V^L \tag{44.17}\]

\[ b = E[V \mid \text{sell order}] = \frac{p(1-\mu)}{(1-\mu) + (1-p)\mu} V^H + \frac{(1-p)(1-\mu) + (1-p)\mu}{(1-\mu) + (1-p)\mu} V^L \tag{44.18}\]

Chênh lệch giá \(s = a - b\) dương bất cứ khi nào \(\mu > 0\), và tăng theo cả tỷ lệ nhà giao dịch có thông tin (\(\mu\)) lẫn mức bất đối xứng thông tin (\(V^H - V^L\)). Sự phân rã này mang tính nền tảng: chênh lệch giá bù đắp cho người cung cấp thanh khoản chi phí lựa chọn đối nghịch khi giao dịch với đối tác có thông tin.

44.5.3 PIN: Xác suất giao dịch có thông tin

Easley và c.s. (1996) mở rộng khuôn khổ Glosten-Milgrom sang bối cảnh động và xây dựng thước đo Xác suất giao dịch có thông tin (PIN), có thể ước lượng từ dữ liệu giao dịch. Mô hình giả định rằng trong mỗi ngày giao dịch, một sự kiện thông tin xảy ra với xác suất \(\alpha\). Với điều kiện có sự kiện xảy ra, đó là tin xấu với xác suất \(\delta\). Các nhà giao dịch có thông tin đến với tốc độ \(\mu\); những người mua và người bán không có thông tin đến với tốc độ lần lượt là \(\varepsilon_b\) và \(\varepsilon_s\).

Hàm hợp lý của việc quan sát \(B_t\) lệnh mua và \(S_t\) lệnh bán trong ngày \(t\) là:

\[ \mathcal{L}(B_t, S_t | \boldsymbol{\theta}) = (1-\alpha) f(B_t|\varepsilon_b) f(S_t|\varepsilon_s) + \alpha\delta \cdot f(B_t|\varepsilon_b) f(S_t|\varepsilon_s + \mu) + \alpha(1-\delta) \cdot f(B_t|\varepsilon_b + \mu) f(S_t|\varepsilon_s) \tag{44.19}\]

trong đó \(f(\cdot|\lambda)\) là hàm mật độ Poisson với tốc độ \(\lambda\), và \(\boldsymbol{\theta} = (\alpha, \delta, \mu, \varepsilon_b, \varepsilon_s)\). Khi đó PIN là:

\[ \text{PIN} = \frac{\alpha \mu}{\alpha \mu + \varepsilon_b + \varepsilon_s} \tag{44.20}\]

Hiện mã
def pin_log_likelihood(params, data):
    """
    Compute negative log-likelihood for the Easley-Kiefer-O'Hara-Paperman
    (1996) PIN model.

    Parameters
    ----------
    params : array
        [alpha, delta, mu, eps_b, eps_s]
    data : DataFrame
        Columns: buys, sells (daily counts).

    Returns
    -------
    float : Negative log-likelihood.
    """
    alpha, delta, mu, eps_b, eps_s = params

    # Parameter bounds
    if (alpha < 0 or alpha > 1 or delta < 0 or delta > 1 or
        mu < 0 or eps_b < 0 or eps_s < 0):
        return 1e15

    B = data["buys"].values
    S = data["sells"].values

    # Use log-sum-exp for numerical stability
    # Three components: no event, bad news, good news
    log_L = np.zeros(len(B))

    for i in range(len(B)):
        b, s = B[i], S[i]

        # Log-Poisson components
        def log_poisson(k, lam):
            if lam <= 0:
                return -1e10 if k > 0 else 0
            return k * np.log(lam) - lam - np.sum(np.log(np.arange(1, k + 1)))

        # No event
        c1 = (np.log(1 - alpha + 1e-15) +
              log_poisson(b, eps_b) + log_poisson(s, eps_s))

        # Bad news (extra sells)
        c2 = (np.log(alpha * delta + 1e-15) +
              log_poisson(b, eps_b) + log_poisson(s, eps_s + mu))

        # Good news (extra buys)
        c3 = (np.log(alpha * (1 - delta) + 1e-15) +
              log_poisson(b, eps_b + mu) + log_poisson(s, eps_s))

        # Log-sum-exp
        max_c = max(c1, c2, c3)
        log_L[i] = max_c + np.log(
            np.exp(c1 - max_c) + np.exp(c2 - max_c) + np.exp(c3 - max_c)
        )

    return -np.sum(log_L)


def estimate_pin(buys, sells, n_starts=10):
    """
    Estimate PIN via MLE with multiple random starts.

    Returns
    -------
    dict : Estimated parameters and PIN value.
    """
    data = pd.DataFrame({"buys": buys, "sells": sells})
    best_result = None
    best_nll = np.inf

    avg_b = data["buys"].mean()
    avg_s = data["sells"].mean()

    for _ in range(n_starts):
        # Random initial values
        alpha0 = np.random.uniform(0.1, 0.8)
        delta0 = np.random.uniform(0.2, 0.8)
        mu0 = np.random.uniform(0, max(avg_b, avg_s))
        eps_b0 = avg_b * np.random.uniform(0.5, 1.5)
        eps_s0 = avg_s * np.random.uniform(0.5, 1.5)

        try:
            result = minimize(
                pin_log_likelihood,
                x0=[alpha0, delta0, mu0, eps_b0, eps_s0],
                args=(data,),
                method="L-BFGS-B",
                bounds=[(0.01, 0.99), (0.01, 0.99),
                        (0.01, None), (0.01, None), (0.01, None)],
                options={"maxiter": 1000}
            )

            if result.fun < best_nll:
                best_nll = result.fun
                best_result = result
        except Exception:
            continue

    if best_result is None:
        return {"pin": np.nan}

    alpha, delta, mu, eps_b, eps_s = best_result.x
    pin = alpha * mu / (alpha * mu + eps_b + eps_s)

    return {
        "alpha": alpha,
        "delta": delta,
        "mu": mu,
        "eps_b": eps_b,
        "eps_s": eps_s,
        "pin": pin,
        "log_likelihood": -best_nll
    }
Hiện mã
# Estimate PIN for a cross-section of stocks
# Using daily buy/sell counts from Lee-Ready classification
trade_counts = dc.get_trade_counts(
    start_date="2023-01-01",
    end_date="2023-12-31"
)

# Sample of stocks for estimation (PIN is computationally intensive)
top_stocks = (
    trade_counts.groupby("ticker")
    .agg(n_days=("date", "nunique"))
    .query("n_days >= 200")
    .index[:50]
)

pin_estimates = []
for ticker in top_stocks:
    stock_data = trade_counts[trade_counts["ticker"] == ticker]
    result = estimate_pin(
        stock_data["buys"].values,
        stock_data["sells"].values,
        n_starts=5
    )
    result["ticker"] = ticker
    pin_estimates.append(result)

pin_df = pd.DataFrame(pin_estimates).dropna(subset=["pin"])
print(f"PIN estimates for {len(pin_df)} stocks")
print(f"  Mean PIN: {pin_df['pin'].mean():.4f}")
print(f"  Median PIN: {pin_df['pin'].median():.4f}")
Hiện mã
(
    p9.ggplot(pin_df, p9.aes(x="pin"))
    + p9.geom_histogram(bins=25, fill="#2E5090", alpha=0.7)
    + p9.geom_vline(
        xintercept=pin_df["pin"].median(),
        linetype="dashed", color="#C0392B", size=0.8
    )
    + p9.labs(
        x="PIN",
        y="Count",
        title="Distribution of Informed Trading Probability"
    )
    + p9.theme_minimal()
    + p9.theme(figure_size=(10, 5))
)
Hình 44.6

44.5.4 PIN và định giá tài sản

Easley, Hvidkjaer, và O’hara (2002) lập luận rằng PIN cần được định giá trong mặt cắt ngang của lợi nhuận kỳ vọng: các cổ phiếu có bất đối xứng thông tin cao hơn đòi hỏi một phần bù rủi ro để bù đắp cho các nhà đầu tư không có thông tin. Chúng ta kiểm định dự báo này bằng các hồi quy Fama-MacBeth.

Hiện mã
# Merge PIN with firm characteristics and forward returns
pin_chars = pin_df[["ticker", "pin"]].merge(
    dc.get_firm_characteristics(
        start_date="2023-01-01",
        end_date="2023-12-31"
    ).groupby("ticker").last().reset_index()[
        ["ticker", "log_size", "book_to_market", "momentum_12m"]
    ],
    on="ticker",
    how="inner"
)

# Forward 12-month returns (2024)
forward_returns = dc.get_annual_returns(
    start_date="2024-01-01",
    end_date="2024-12-31"
)

pin_returns = pin_chars.merge(
    forward_returns[["ticker", "annual_ret"]],
    on="ticker",
    how="inner"
)

# Cross-sectional regression
if len(pin_returns) > 20:
    cs_model = sm.OLS(
        pin_returns["annual_ret"],
        sm.add_constant(
            pin_returns[["pin", "log_size", "book_to_market", "momentum_12m"]]
        )
    ).fit(cov_type="HC1")

    print("Cross-Sectional Return Regression with PIN:")
    for var in ["pin", "log_size", "book_to_market", "momentum_12m"]:
        print(f"  {var}: {cs_model.params[var]:.4f} "
              f"(t = {cs_model.tvalues[var]:.3f})")

44.5.5 Các thách thức ước lượng trong mô hình sổ lệnh giới hạn

Ước lượng cấu trúc các mô hình sổ lệnh giới hạn đối mặt với một số thách thức đặc thù của thị trường Việt Nam:

Hiệu ứng bước giá. Các sàn giao dịch Việt Nam dùng bước giá rời rạc thay đổi theo mức giá. Ở mức giá thấp, bước giá tối thiểu chiếm tỷ trọng lớn của giá, làm chênh lệch giá rộng ra một cách giả tạo và bóp méo các tham số cấu trúc. Các mô hình Glosten-Milgrom và PIN chuẩn giả định giá liên tục; việc điều chỉnh chúng cho lưới giá rời rạc đòi hỏi các sửa đổi do Bollen, Smith, và Whaley (2004) đề xuất.

Biên độ giá. Khi một cổ phiếu chạm biên độ giá, sổ lệnh thực chất bị đóng băng ở một phía. Các lệnh tích tụ nhưng không thể khớp cho đến khi biên độ được nới. Điều này tạo ra vấn đề kiểm duyệt (censoring) tương tự như việc kiểm duyệt do biên độ giá trong phân phối lợi nhuận: dòng lệnh quan sát được là phiên bản bị cắt cụt của dòng lệnh tiềm ẩn.

Dữ liệu thiếu. Các ảnh chụp toàn bộ sổ lệnh ở tần suất cao không phải lúc nào cũng có sẵn đối với cổ phiếu Việt Nam. Ước lượng PIN chỉ cần số lệnh mua/bán hằng ngày, nên khả thi với dữ liệu tần suất thấp hơn. Nhưng các mô hình phong phú hơn (sổ lệnh giới hạn động của Foucault (1999), mô hình thời gian liên tục của Roşu (2009)) đòi hỏi dữ liệu sổ lệnh theo từng giao dịch mà có thể không có sẵn cho các cổ phiếu nhỏ hơn.

Đặc điểm thể chế. Sự tồn tại song song của HOSE (khớp lệnh liên tục) và HNX (với các phiên khớp lệnh định kỳ cho cổ phiếu kém thanh khoản) tạo ra sự không đồng nhất trong mô hình cấu trúc phù hợp. Một mô hình được ước lượng trên dữ liệu giao dịch liên tục của HOSE không áp dụng trực tiếp được cho các cổ phiếu khớp lệnh định kỳ của HNX.

44.6 Khi nào mô hình cấu trúc đáng được sử dụng

44.6.1 Khung ra quyết định

Ước lượng cấu trúc không phải lúc nào cũng là công cụ phù hợp. Độ phức tạp tăng thêm, yêu cầu về dữ liệu và rủi ro mô hình phải được biện minh bởi câu hỏi nghiên cứu. Bảng 44.6 đưa ra một khung ra quyết định.

Bảng 44.6: Khi nào chọn dạng cấu trúc so với dạng rút gọn
Tiêu chí Ưu tiên dạng cấu trúc Ưu tiên dạng rút gọn
Câu hỏi nghiên cứu Phân tích phản thực tế hoặc đánh giá chính sách Tác động nhân quả của biến thiên quan sát được
Dữ liệu sẵn có Dữ liệu vi mô phong phú (giao dịch, vị thế nắm giữ) Dữ liệu bảng chuẩn (lợi nhuận, dữ liệu cơ bản)
Môi trường thể chế Ổn định (giả định mô hình hợp lý) Thay đổi nhanh (mô hình có thể bị đặc tả sai)
Số lượng tham số Ít yếu tố gốc, nhiều đại lượng quan sát được Nhiều tham số, ít giả định nhận diện
Tạp chí công bố JF, RFS, Econometrica, JPE JFE, RFS, MS, JFQA
Ngân sách tính toán Vài tuần đến vài tháng ước lượng Vài giờ đến vài ngày

44.6.2 Yêu cầu về dữ liệu

Các mô hình cấu trúc rất “đói” dữ liệu. Bảng 44.7 tóm tắt các yêu cầu dữ liệu tối thiểu cho từng lớp mô hình được đề cập trong chương này.

Bảng 44.7: Yêu cầu về dữ liệu cho ước lượng cấu trúc
Mô hình Dữ liệu tối thiểu Dữ liệu lý tưởng Mức sẵn có ở Việt Nam
Hệ thống cầu KY Vị thế nắm giữ của tổ chức theo quý + giá Báo cáo tương đương 13F + dòng vốn quỹ Một phần (công bố theo nửa năm)
Almgren-Chriss Khối lượng, chênh lệch giá, biến động theo ngày Dữ liệu từng lệnh trong ngày Tốt (HOSE cung cấp)
Rock/IPO Giá chào bán, giá đóng cửa ngày đầu, phân bổ Giá đặt mua và phân bổ ở cấp nhà đầu tư Hạn chế (có dữ liệu đấu giá)
PIN Số lệnh mua/bán theo ngày (Lee-Ready) Dữ liệu giao dịch và báo giá theo từng giao dịch Trung bình (cần phân loại giao dịch)
Glosten-Milgrom Giá mua/bán tốt nhất, chiều giao dịch Ảnh chụp toàn bộ sổ lệnh Trung bình (có dữ liệu mức giá tốt nhất của sổ lệnh)

44.6.3 Chi phí tính toán

Ước lượng cấu trúc tốn kém hơn hồi quy dạng rút gọn nhiều bậc độ lớn. Chi phí đến từ ba nguồn:

Đánh giá hàm hợp lý. Mỗi lần đánh giá hàm hợp lý hoặc hàm moment cấu trúc đòi hỏi giải mô hình với các tham số cho trước. Với các mô hình động (ví dụ: mô hình tồn kho, lựa chọn rời rạc động), điều này đồng nghĩa với việc giải một bài toán quy hoạch động ở mỗi vòng lặp.

Tối ưu hóa. Hàm mục tiêu GMM hoặc MLE thường không lồi, đòi hỏi nhiều điểm xuất phát và các thuật toán tối ưu toàn cục. Mô hình PIN với 5 tham số cần \(\sim 10\) điểm xuất phát ngẫu nhiên; một mô hình phong phú hơn với \(\sim 20\) tham số có thể cần hàng trăm.

Suy luận. Sai số chuẩn của các tham số cấu trúc thường đòi hỏi bootstrap (vì phân phối tiệm cận không chuẩn hoặc phương pháp delta không đáng tin cậy), làm ngân sách tính toán tăng thêm một hệ số nhân từ 200–1000.

Hiện mã
# Illustration: computational cost comparison
import time

# Reduced form: OLS regression
n_obs = 100000
X_sim = np.random.randn(n_obs, 5)
y_sim = X_sim @ np.random.randn(5) + np.random.randn(n_obs)

start = time.time()
for _ in range(100):
    sm.OLS(y_sim, sm.add_constant(X_sim)).fit()
ols_time = (time.time() - start) / 100

# Structural: PIN estimation (single stock)
sim_buys = np.random.poisson(50, 250)
sim_sells = np.random.poisson(45, 250)

start = time.time()
pin_result = estimate_pin(sim_buys, sim_sells, n_starts=5)
pin_time = time.time() - start

print(f"Computational Cost Comparison:")
print(f"  OLS (100k obs): {ols_time*1000:.1f} ms per estimation")
print(f"  PIN (250 days, 5 starts): {pin_time:.1f} s per stock")
print(f"  Ratio: {pin_time / ols_time:.0f}x")
Computational Cost Comparison:
  OLS (100k obs): 57.2 ms per estimation
  PIN (250 days, 5 starts): 31.2 s per stock
  Ratio: 545x

44.6.4 Rủi ro diễn giải

Rủi ro chính của ước lượng cấu trúc là đặc tả sai mô hình. Nếu mô hình sai, các tham số ước lượng không có ý nghĩa kinh tế và các dự báo phản thực tế không đáng tin cậy. Một số chiến lược giúp giảm thiểu rủi ro này:

Kiểm định đặc tả. Các ràng buộc quá nhận diện (nhiều moment hơn số tham số) cung cấp kiểm định \(J\) của Hansen cho độ phù hợp của mô hình. Việc bác bỏ gợi ý đặc tả sai, mặc dù kiểm định có sức mạnh thấp trong mẫu nhỏ.

So sánh mô hình. Ước lượng nhiều mô hình lồng nhau hoặc không lồng nhau và so sánh độ phù hợp. Nếu một mô hình đơn giản hơn phù hợp với dữ liệu ngang bằng, hãy ưu tiên nó (dao cạo Occam).

Phân tích độ nhạy. Báo cáo các tham số cấu trúc thay đổi ra sao dưới các giả định thay thế hợp lý (ví dụ: các dạng hàm khác nhau cho hàm tác động, các giả định phân phối khác nhau cho định giá).

Kiểm định ngoài mẫu. Ước lượng mô hình trên một giai đoạn mẫu và kiểm tra các dự báo của nó trên một mẫu giữ lại (holdout). Các mô hình cấu trúc khớp trong mẫu nhưng thất bại ngoài mẫu nhiều khả năng bị quá khớp.

44.6.5 Kỳ vọng của các tạp chí

Các bài báo cấu trúc trên những tạp chí tài chính hàng đầu phải đáp ứng các tiêu chuẩn cụ thể:

Sự rõ ràng về nhận diện. Bài báo phải nêu rõ tham số nào được nhận diện, từ những moment nào, và biến thiên nào trong dữ liệu cung cấp nhận diện. Lời phê bình của Rust (1987) rằng nếu không có nhận diện rõ ràng thì ước lượng cấu trúc chỉ là khớp đường cong (curve-fitting) phải được giải quyết trực diện.

Độ tin cậy của phân tích phản thực tế. Các bài tập phản thực tế phải có động cơ kinh tế và phạm vi các kịch bản phản thực tế không nên ngoại suy quá xa so với dữ liệu.

Độ vững theo đặc tả. Những người phản biện sẽ hỏi điều gì xảy ra dưới các giả định phân phối thay thế, các điều kiện moment thay thế và các đặc điểm mô hình thay thế.

Tính minh bạch. Mã nguồn và dữ liệu cần được công khai. Ước lượng cấu trúc đủ phức tạp để khả năng tái lập là mối quan tâm hàng đầu.

44.7 Tóm tắt

Chương này giới thiệu ước lượng cấu trúc như một phương pháp luận riêng biệt cho kinh tế học tài chính, một phương pháp đánh đổi sự đơn giản và minh bạch của phân tích dạng rút gọn để lấy khả năng ước lượng các yếu tố kinh tế gốc và đánh giá chính sách phản thực tế. Chúng ta đã triển khai bốn lớp mô hình cấu trúc: hệ thống cầu cho tài sản tài chính, các mô hình thực thi tối ưu và chi phí giao dịch, các mô hình đấu giá IPO, và các mô hình sổ lệnh giới hạn về giao dịch có thông tin.

Cách tiếp cận hệ thống cầu của Koijen và Yogo (2019) cho thấy các nhà đầu tư tổ chức ở Việt Nam có độ co giãn cầu không đồng nhất theo các đặc điểm, trong đó nhà đầu tư nước ngoài có các mẫu hình độ nhạy khác với các tổ chức trong nước. Ước lượng tác động thị trường xác nhận tính phổ quát của quy luật căn bậc hai, trong khi khuôn khổ Almgren-Chriss cung cấp chuẩn thực thi tối ưu cho các lệnh lớn của tổ chức. Mô hình IPO của Rock định lượng phần bù bất đối xứng thông tin gắn trong mức định giá thấp khi IPO ở Việt Nam, và mô hình PIN ước lượng xác suất giao dịch có thông tin trong mặt cắt ngang.

Mỗi mô hình đều có sự đánh đổi giữa các câu hỏi kinh tế mà nó có thể trả lời và các giả định mà nó đòi hỏi. Quyết định dùng ước lượng cấu trúc nên được dẫn dắt bởi câu hỏi nghiên cứu (cụ thể là liệu phân tích phản thực tế có thiết yếu hay không) và được điều tiết bởi sự đánh giá trung thực về việc các giả định của mô hình có đủ đáng tin cậy trong môi trường thể chế Việt Nam hay không. Khi có, các mô hình cấu trúc đem lại những hiểu biết mà không một lượng hồi quy dạng rút gọn nào có thể mang lại.