Lý thuyết Danh mục Hiện đại (Modern Portfolio Theory - MPT) do Harry Markowitz khởi xướng năm 1952 mô hình hóa việc phân bổ tài sản như một bài toán tối ưu hóa toàn phương có ràng buộc. Mặc dù sở hữu vẻ đẹp toán học thanh lịch, phương pháp Mean-Variance Optimization (MVO) cổ điển gặp phải điểm yếu nghiêm trọng trong thực tiễn tài chính gọi là Lời nguyền Markowitz (Markowitz Curse): công cụ tối ưu hóa khuếch đại sai số ước lượng trong ma trận hiệp phương sai lịch sử, dẫn đến tỷ trọng phân bổ cực đoan và phản trực giác.
Trong bài viết này, chúng ta nghiên cứu Hierarchical Risk Parity (HRP)—được đề xuất bởi Marcos López de Prado—phương pháp ứng dụng phân cụm đồ thị để xây dựng danh mục đầu tư đa dạng hóa vững chắc mà không cần nghịch đảo ma trận hiệp phương sai có điều kiện xấu.
1. Sự Bất ổn định của Ma trận Nghịch đảo Hiệp phương sai
Cho $N$ tài sản với lợi nhuận kỳ vọng $\boldsymbol{\mu} \in \mathbb{R}^N$ và ma trận hiệp phương sai $\boldsymbol{\Sigma} \in \mathbb{R}^{N \times N}$, vectơ trọng số danh mục có phương sai tối thiểu toàn cầu $\mathbf{w}^*$ được xác định bởi:
$$\mathbf{w}^* = \frac{\boldsymbol{\Sigma}^{-1} \mathbf{1}}{\mathbf{1}^T \boldsymbol{\Sigma}^{-1} \mathbf{1}}$$Khi các tài sản xuất hiện hiện tượng đa cộng tuyến (tương quan chéo tăng vọt trong giai đoạn thị trường khủng hoảng), $\boldsymbol{\Sigma}$ tiệm cận trạng thái suy biến với số điều kiện $\kappa(\boldsymbol{\Sigma}) \to \infty$. Những biến động nhỏ trong mẫu dữ liệu sẽ gây ra sự xáo trộn khổng lồ trong $\boldsymbol{\Sigma}^{-1}$, dẫn đến danh mục không ổn định.
2. Ba Bước Triển khai của Hierarchical Risk Parity (HRP)
HRP thay thế phép nghịch đảo đại số tuyến tính bằng thuật toán phân cụm đồ thị phân cấp, thực thi qua ba giai đoạn tất định:
2.1 Phân cụm Cây qua Khoảng cách Tương quan
Trước tiên, ta chuyển đổi ma trận tương quan thực nghiệm $\mathbf{R} \in [-1, 1]^{N \times N}$ thành ma trận khoảng cách mêtric $\mathbf{D}$:
$$D_{i,j} = \sqrt{\frac{1 - R_{i,j}}{2}} \in [0, 1]$$Sử dụng thuật toán phân cụm tích tụ liên kết đơn (single-linkage agglomerative clustering), ta dựng cây dendrogram biểu diễn cấu trúc quan hệ giữa các tài sản.
2.2 Chuẩn Chéo hóa (Quasi-Diagonalization)
Ta sắp xếp lại các hàng và cột của $\boldsymbol{\Sigma}$ sao cho các tài sản thuộc cùng một cụm nằm liền kề nhau dọc theo đường chéo chính, bộc lộ các phân ngành tài chính dạng khối mà không cần gán nhãn trước.
2.3 Phân đôi Đệ quy & Phân bổ Nghịch đảo Phương sai
Ta đệ quy phân đôi cây dendrogram. Với mỗi điểm phân tách chia tài sản thành cụm trái $\mathcal{L}$ và cụm phải $\mathcal{R}$, phương sai cụm được tính:
$$V_{\mathcal{L}} = \mathbf{w}_{\mathcal{L}}^T \boldsymbol{\Sigma}_{\mathcal{L}} \mathbf{w}_{\mathcal{L}}, \quad V_{\mathcal{R}} = \mathbf{w}_{\mathcal{R}}^T \boldsymbol{\Sigma}_{\mathcal{R}} \mathbf{w}_{\mathcal{R}}$$Tỷ trọng phân bổ giữa hai nhánh được điều phối bởi nghịch đảo phương sai cụm:
$$\alpha = 1 - \frac{V_{\mathcal{L}}}{V_{\mathcal{L}} + V_{\mathcal{R}}}, \quad \mathbf{w}_{\mathcal{L}} \leftarrow \alpha \mathbf{w}_{\mathcal{L}}, \quad \mathbf{w}_{\mathcal{R}} \leftarrow (1-\alpha) \mathbf{w}_{\mathcal{R}}$$3. Cài đặt Python Hoàn chỉnh cho HRP
Dưới đây là mã nguồn Python hiệu năng cao sử dụng numpy, scipy và pandas:
import numpy as np
import pandas as pd
from scipy.cluster.hierarchy import linkage
from scipy.spatial.distance import squareform
def get_quasi_diag(link):
link = link.astype(int)
sort_ix = pd.Series([link[-1, 0], link[-1, 1]])
num_items = link[-1, 3]
while sort_ix.max() >= num_items:
sort_ix.index = range(0, sort_ix.shape[0] * 2, 2)
df0 = sort_ix[sort_ix >= num_items]
i = df0.index
j = df0.values - num_items
sort_ix[i] = link[j, 0]
df0 = pd.Series(link[j, 1], index=i + 1)
sort_ix = pd.concat([sort_ix, df0]).sort_index()
sort_ix.index = range(sort_ix.shape[0])
return sort_ix.tolist()
def get_rec_bipart(cov, sort_ix):
w = pd.Series(1, index=sort_ix)
c_items = [sort_ix]
while len(c_items) > 0:
c_items = [i[j:k] for i in c_items for j, k in ((0, len(i) // 2), (len(i) // 2, len(i))) if len(i) > 1]
for i in range(0, len(c_items), 2):
c_items0 = c_items[i]
c_items1 = c_items[i + 1]
c_var0 = get_cluster_var(cov, c_items0)
c_var1 = get_cluster_var(cov, c_items1)
alpha = 1 - c_var0 / (c_var0 + c_var1)
w[c_items0] *= alpha
w[c_items1] *= 1 - alpha
return w
def get_cluster_var(cov, c_items):
cov_slice = cov.loc[c_items, c_items]
w_inv = 1 / np.diag(cov_slice)
w = w_inv / w_inv.sum()
return np.dot(np.dot(w, cov_slice), w)