Advanced · Expected Returns · บทย่อย 3
Black–Litterman: รวมค่าตั้งต้นกับ Views
ถ้าค่าตั้งต้นบอกว่าผลตอบแทนคาดหวังอยู่ที่ 4% แต่งานวิเคราะห์ของเราประเมินได้ 6% เราควรใช้ตัวเลขใด และควรเชื่อตัวเลขนั้นแค่ไหน?
บทก่อนหน้า Implied Returns และ Active Views เตรียมค่าตั้งต้น จากน้ำหนักอ้างอิง และเขียนมุมมองเพิ่มเติมเป็น แล้ว ขั้นตอนของ Black–Litterman ในบทนี้คือรวมข้อมูลสองส่วน โดยให้ความไม่แน่นอนของแต่ละส่วนกำหนดขนาดการปรับค่าเฉลี่ย จากนั้นจึงนำผลไปเลือกน้ำหนักพอร์ตภายใต้ข้อจำกัดที่ต้องการ
ตัวอย่างทั้งหมดเป็นข้อมูลสมมติที่เขียนขึ้นใหม่ ใช้ผลตอบแทนคาดหวังส่วนเกินจากอัตราปลอดความเสี่ยงในช่วงหนึ่งปีเดียวกัน หน่วยทศนิยม เช่น 0.04 หมายถึง 4% ต่อปี ไม่มีต้นทุน ภาษี หรือข้อมูลราคาตลาดจริง โค้ดรันตามลำดับใน Notebook ใหม่ได้ด้วย NumPy, pandas และ SciPy โดยไม่ต้องรันบทก่อนหน้า ดาวน์โหลด Notebook
ลองรวมค่าประมาณเพียงตัวเดียวก่อน
สมมติว่าเราต้องการประมาณค่าเฉลี่ยผลตอบแทนส่วนเกินที่ยังไม่รู้ เรียกค่านี้ว่า ข้อมูลก่อนเพิ่ม View หรือ prior ของเราระบุว่า มีค่าเฉลี่ย 4% และ SD ของความไม่แน่นอนเท่ากับ 2 จุดเปอร์เซ็นต์ เขียนด้วยการแจกแจง Normal ได้ว่า
เลขตัวที่สองใน เป็น variance จึงต้องยกกำลังสอง ส่วน 2 จุดเปอร์เซ็นต์นี้วัดความไม่แน่นอนเกี่ยวกับ ไม่ใช่ SD ของผลตอบแทนที่สินทรัพย์จะให้ในปีหน้า
งานวิเคราะห์อีกส่วนให้ View ว่า อยู่ที่ 6% โดยมีความคลาดเคลื่อนแบบ Normal ที่ SD เท่ากับ 1 จุดเปอร์เซ็นต์ เราสมมติว่าความคลาดเคลื่อนของ View เป็นอิสระจากค่า ที่มี prior ข้างต้น การรวมข้อมูลภายใต้สมมติฐานนี้ใช้น้ำหนักแปรผกผันกับ variance เรียก ว่า precision
Prior มี precision 2,500 ส่วน View มี precision 10,000 จึงให้น้ำหนัก 20% กับ prior และ 80% กับ View ค่าเฉลี่ยหลังรวม หรือ posterior เท่ากับ
import numpy as np
import pandas as pd
from scipy.optimize import minimize
scalar_prior_mean, scalar_prior_se = 0.04, 0.02
scalar_view_mean, scalar_view_se = 0.06, 0.01
scalar_prior_precision = 1 / scalar_prior_se**2
scalar_view_precision = 1 / scalar_view_se**2
scalar_posterior_var = 1 / (scalar_prior_precision + scalar_view_precision)
scalar_posterior_mean = scalar_posterior_var * (
scalar_prior_precision * scalar_prior_mean
+ scalar_view_precision * scalar_view_mean
)
print(f"Posterior expected return: {scalar_posterior_mean:.4%}")
print(f"Posterior uncertainty SD: {np.sqrt(scalar_posterior_var):.4%}")
print("Weights on prior and view:",
scalar_posterior_var * np.array([scalar_prior_precision, scalar_view_precision]))
SD ของความไม่แน่นอนหลังรวมเท่ากับประมาณ 0.8944 จุดเปอร์เซ็นต์ ต่ำกว่าก่อนรวม เพราะแบบจำลองถือว่าเราได้รับข้อมูลเพิ่มที่มีประโยชน์ นี่เป็นผลภายใต้สมมติฐานเรื่องความเป็นอิสระและความคลาดเคลื่อนที่ระบุไว้ หาก View เพียงนำตัวเลขเดิมมาพูดซ้ำ เราจะให้น้ำหนักเหมือนเป็นข้อมูลใหม่ไม่ได้
ในโค้ด **2 คือยกกำลังสอง และ np.sqrt คือรากที่สอง ส่วนรูปแบบ :.4% คูณตัวเลขทศนิยมด้วย 100 เพื่อแสดงเป็นเปอร์เซ็นต์สี่ตำแหน่ง ตัวแปรยังเก็บค่าที่ไม่ปัดเศษไว้สำหรับคำนวณต่อ
กลับมาที่สามสินทรัพย์และสอง Views
ใช้สินทรัพย์สมมติ A/B/C ชุดเดียวกับบทก่อนหน้า น้ำหนักอ้างอิงคือ และ covariance ของผลตอบแทนส่วนเกินหนึ่งปีคือ
รากที่สองของแนวทแยงให้ SD ผลตอบแทน 20%, 15% และ 10% ต่อปีตามลำดับ เรากำหนดค่าความเกลียดความเสี่ยงสำหรับสร้าง prior เป็น จึงได้ implied excess returns
นี่เป็นผลตอบแทนที่สอดคล้องกับน้ำหนักอ้างอิงภายใต้ covariance และแบบจำลองที่เลือก ไม่ใช่ผลตอบแทนอนาคตที่อ่านออกมาจากราคาตลาดได้โดยปราศจากสมมติฐาน
สอง Views ของเราคือ C มีผลตอบแทนส่วนเกินคาดหวัง 2.5% และ A มีผลตอบแทนคาดหวังสูงกว่า B อยู่ 2 จุดเปอร์เซ็นต์:
แถวแรกเลือกค่าเฉลี่ยของ C ส่วนแถวที่สองนำค่าเฉลี่ย A ลบ B ดังนั้น มีสองค่า ตรงกับจำนวน Views ไม่ใช่จำนวนสินทรัพย์ หาก View แรกเป็นผลตอบแทนรวมแทนผลตอบแทนส่วนเกิน ต้องหักอัตราปลอดความเสี่ยงของช่วงเดียวกันก่อน ส่วน View แบบ A ลบ B หักอัตราปลอดความเสี่ยงเดียวกันออกจากทั้งสองฝั่งแล้วหายไป
กำหนดความไม่แน่นอนของ prior เป็น โดย และใช้กฎสาธิตของ Lab สำหรับ ความไม่แน่นอนของ Views: เก็บเฉพาะแนวทแยงของ เป็น ค่า 0.05 และกฎนี้เป็นข้อกำหนดของตัวอย่าง ไม่ได้ประมาณมาจากความแม่นยำของผู้วิเคราะห์
assets = ["A", "B", "C"]
covariance = np.array([[0.0400, 0.0120, 0.0080],
[0.0120, 0.0225, 0.0045],
[0.0080, 0.0045, 0.0100]])
benchmark_weights = np.array([0.5, 0.3, 0.2])
risk_aversion = 2.5
pi = risk_aversion * covariance @ benchmark_weights
P = np.array([[0.0, 0.0, 1.0], [1.0, -1.0, 0.0]])
Q = np.array([0.025, 0.020])
tau = 0.05
prior_mean_cov = tau * covariance
projected_prior_cov = P @ prior_mean_cov @ P.T
omega = np.diag(np.diag(projected_prior_cov))
print("Prior excess returns (%):", np.round(pi * 100, 4))
print("View surprises (percentage points):", np.round((Q - P @ pi) * 100, 4))
print("Projected prior covariance:\n", projected_prior_cov)
print("View-error covariance:\n", omega)
@ คูณเมทริกซ์ ส่วน .T สลับแถวกับคอลัมน์ np.diag ครั้งแรกดึงแนวทแยงออกมาเป็นเวกเตอร์ และครั้งที่สองนำเวกเตอร์นั้นไปสร้างเมทริกซ์แนวทแยง จึงได้
เมทริกซ์แรกวัดความไม่แน่นอนของค่าที่ prior คาดสำหรับสอง Views ส่วนเมทริกซ์หลังวัดความคลาดเคลื่อนของแหล่งข้อมูล Views ที่เราใส่เพิ่ม การตั้งช่องนอกแนวทแยงของ เป็นศูนย์จึงเป็นสมมติฐานว่าความคลาดเคลื่อนของ Views เป็นอิสระ ไม่ได้หมายความว่าพอร์ตที่ Views กล่าวถึงไม่มีความสัมพันธ์กัน
ความต่าง หรือ view surprise เท่ากับ View ของ C สูงกว่า prior 0.6625 จุดเปอร์เซ็นต์ แต่ View A ชนะ B อยู่ 2 จุดเปอร์เซ็นต์ต่ำกว่า prior spread ที่ 2.8875 จุดเปอร์เซ็นต์ จึงเป็นการปรับความคาดหวังส่วนต่าง A/B ลง แม้ประโยคของ View จะยังใช้คำว่า A ชนะ B
ปรับค่าเฉลี่ยด้วยขนาดความไม่แน่นอน
แบบจำลองกำหนดให้ และ โดย และ เป็นอิสระจาก เราจะใช้สัญลักษณ์ สำหรับค่าเฉลี่ย posterior ของ
เริ่มจากเมทริกซ์ขนาดสอง Views คูณสอง Views:
จากนั้นสร้างเมทริกซ์ตัวคูณการปรับ หรือ gain:
สูตรมีความหมายว่าเริ่มจาก prior แล้วบวกการปรับตามความต่างของ Views ส่วน กำหนดว่าความต่างแต่ละ View ส่งต่อไปยังค่าเฉลี่ยแต่ละสินทรัพย์เท่าใด โดยใช้ทั้ง covariance และความไม่แน่นอนที่ระบุไว้
| ตัวแปร | ขนาดในตัวอย่าง | ความหมาย |
|---|---|---|
| 3 ค่า | ค่าเฉลี่ยผลตอบแทนส่วนเกินของ A/B/C | |
| 2 × 3 | แปลงค่าเฉลี่ยสินทรัพย์เป็นค่าของสอง Views | |
| 2 ค่า | ความต่างของ Views จาก prior | |
| 2 × 2 | ความไม่แน่นอนรวมในพื้นที่ของ Views | |
| 3 × 2 | ส่งความต่างสองค่าไปปรับค่าเฉลี่ยสามสินทรัพย์ |
ในโปรแกรมเราไม่จำเป็นต้องสร้าง inverse ทั้งเมทริกซ์ np.linalg.solve(A, B) หา ที่ทำให้ โค้ดต่อไปแก้ แล้ว transpose คำตอบเพื่อให้ได้ โดยใช้ความสมมาตรของเมทริกซ์ covariance
view_system = P @ prior_mean_cov @ P.T + omega
gain = np.linalg.solve(view_system, P @ prior_mean_cov).T
posterior_mean = pi + gain @ (Q - P @ pi)
posterior_mean_cov = prior_mean_cov - gain @ P @ prior_mean_cov
predictive_cov = covariance + posterior_mean_cov
print("Gain matrix:\n", np.round(gain, 6))
print(pd.DataFrame({"Prior excess (%)": pi * 100,
"Posterior excess (%)": posterior_mean * 100}, index=assets).round(4).to_string())
print("Posterior view means (%):", np.round(P @ posterior_mean * 100, 4))
print("Posterior uncertainty of the mean:\n", np.round(posterior_mean_cov, 8))
ได้ค่าเฉลี่ย posterior ของ A/B/C ประมาณ 6.2156%, 3.7098% และ 2.1458% ต่อปี ช่อง A ของ gain บอกว่าต้องคูณ surprise ของ View C ด้วย 0.339061 และ surprise ของ View A/B ด้วย 0.348225 จึงคำนวณการปรับ A ได้ประมาณ
ดังนั้นค่าเฉลี่ย A ลดจาก 6.3% เป็นประมาณ 6.2156% การคำนวณพร้อมกันนี้คำนึงถึงความสัมพันธ์ระหว่างสินทรัพย์ ค่าเฉลี่ยของสินทรัพย์ที่ไม่ได้ถูกเลือกโดย View ใดโดยตรงก็อาจเปลี่ยนได้ การผสมจึงไม่ใช่ค่าเฉลี่ยถ่วงน้ำหนักทีละสินทรัพย์แบบตัวอย่างหนึ่งมิติเสมอไป
เมื่ออ่านผลผ่าน เราได้ C ประมาณ 2.1458% และ A ลบ B ประมาณ 2.5057 จุดเปอร์เซ็นต์ Views ยังไม่ถูกบังคับให้เกิดขึ้นตรงตัว เพราะ ระบุว่าข้อมูล Views มีความคลาดเคลื่อน
ความไม่แน่นอนของค่าเฉลี่ยยังไม่ใช่ความเสี่ยงของผลตอบแทน
นอกจากค่าเฉลี่ย posterior แล้ว เราคำนวณ covariance ของความไม่แน่นอนที่เหลือเกี่ยวกับ ได้ด้วย:
ภายใต้โมเดลนี้ หลังรับ Views มีการแจกแจง ถ้าต้องการความเสี่ยงของผลตอบแทนในปีหน้าต้องพิจารณาอีกชั้นหนึ่ง สมมติให้ผลตอบแทนส่วนเกิน เมื่อทราบ มีค่าเฉลี่ย และ covariance ที่ถือว่าทราบแล้ว ความผันผวนรอบค่าเฉลี่ยนี้ยังคงมีอยู่ แม้เราจะรู้ค่าเฉลี่ยอย่างแม่นยำ
การรวมความไม่แน่นอนสองชั้นให้
เราจึงเก็บทั้งสามผลแยกกัน: mean คือ , mean_cov คือ และ covariance + mean_cov คือ predictive covariance อย่านำ เพียงอย่างเดียวไปใช้เป็น covariance ของผลตอบแทนสินทรัพย์
อ่านเพิ่ม: สูตรที่เขียนด้วย precision matrix
หาก และ กลับเมทริกซ์ได้ สูตรเดียวกันเขียนเป็น
นี่เป็นรูปหลายมิติของการบวก precision ในตัวอย่างแรก รูป gain ที่เราใช้ไม่ต้องกลับ โดยตรง จึงใช้กับบางกรณีที่ เป็น singular ได้ ตราบใดที่ ยังแก้ระบบสมการได้ อย่างไรก็ตามการเปลี่ยนรูปสูตรไม่ได้ทำให้ข้อมูลซ้ำหรือระบบ singular ทุกชนิดหายไป
รวมขั้นตอนเป็นฟังก์ชันเพื่อทดลองเปลี่ยนข้อมูลเข้าได้สะดวก ฟังก์ชันนี้ใช้ array หนึ่งมิติสำหรับ และเมทริกซ์สองมิติสำหรับตัวอื่น โดยให้ลำดับสินทรัพย์ตรงกันทุกจุด
def bl_posterior(covariance, pi, P, Q, omega, tau=0.05):
covariance, pi, P, Q, omega = [np.asarray(x, dtype=float)
for x in (covariance, pi, P, Q, omega)]
if (covariance.ndim != 2 or covariance.shape[0] == 0
or covariance.shape[0] != covariance.shape[1] or pi.ndim != 1
or P.ndim != 2 or Q.ndim != 1):
raise ValueError("Need a square covariance, vectors pi/Q and a matrix P")
n, k = len(pi), len(Q)
if covariance.shape != (n, n) or P.shape != (k, n) or omega.shape != (k, k):
raise ValueError("Asset and view dimensions do not match")
if not all(np.isfinite(x).all() for x in (covariance, pi, P, Q, omega)):
raise ValueError("Inputs must be finite")
if not np.isfinite(tau) or tau <= 0:
raise ValueError("Tau must be finite and positive")
if (not np.allclose(covariance, covariance.T, atol=1e-12)
or not np.allclose(omega, omega.T, atol=1e-12)):
raise ValueError("Covariance matrices must be symmetric")
np.linalg.cholesky(covariance) # requires positive definite asset covariance
prior_cov = tau * covariance
if k == 0:
return pi.copy(), prior_cov, covariance + prior_cov
if np.linalg.eigvalsh(omega).min() < -1e-12:
raise ValueError("View-error covariance must be positive semidefinite")
system = P @ prior_cov @ P.T + omega
system_eigenvalues = np.linalg.eigvalsh(system)
if system_eigenvalues.min() <= 1e-12 * system_eigenvalues.max():
raise np.linalg.LinAlgError("View system is singular or too close to singular")
np.linalg.cholesky(system) # redundant exact views may make this singular
update = np.linalg.solve(system, P @ prior_cov).T
mean = pi + update @ (Q - P @ pi)
mean_cov = prior_cov - update @ P @ prior_cov
mean_cov = (mean_cov + mean_cov.T) / 2 # remove roundoff asymmetry
return mean, mean_cov, covariance + mean_cov
bl_mean, bl_M, bl_predictive = bl_posterior(covariance, pi, P, Q, omega, tau)
assert np.allclose(bl_mean, posterior_mean)
assert np.allclose(bl_M, posterior_mean_cov)
print(pd.DataFrame({
"Prior mean uncertainty SD (%)": np.sqrt(np.diag(prior_mean_cov)) * 100,
"Posterior mean uncertainty SD (%)": np.sqrt(np.diag(bl_M)) * 100,
"Conditional return SD (%)": np.sqrt(np.diag(covariance)) * 100,
"Predictive return SD (%)": np.sqrt(np.diag(bl_predictive)) * 100
}, index=assets).round(4).to_string())
สำหรับ C, SD ของความไม่แน่นอนเกี่ยวกับค่าเฉลี่ยลดจากประมาณ 2.2361 เป็น 1.5748 จุดเปอร์เซ็นต์ แต่ SD ของผลตอบแทนที่คาดการณ์ยังอยู่ประมาณ 10.1232% ต่อปี เพราะรวมความผันผวนพื้นฐาน 10% ด้วย การลดความไม่แน่นอนของค่าเฉลี่ยไม่ได้ทำให้การลงทุนไม่มีความเสี่ยง
หาก Views ตรงกับ prior พอดีจน , ค่าเฉลี่ย posterior จะไม่เปลี่ยน แต่ข้อมูล Views ที่กำหนดว่ามีความแม่นยำอาจยังลด ได้ การมีข้อมูลมายืนยันค่าเดิมจึงต่างจากการไม่มี Views เลย
np.asarray(..., dtype=float) ทำให้ข้อมูลเข้าอยู่ในรูป array ตัวเลข np.isfinite ปฏิเสธทั้งค่าที่หายไปและ infinity ส่วน np.linalg.cholesky ตรวจว่าระบบที่ต้องใช้เป็น positive definite โค้ดยังหยุดถ้าเมทริกซ์ของ Views singular หรือใกล้ singular มาก เราควรตรวจว่ามี Views ซ้ำหรือข้อมูลเข้าผิดก่อนเปลี่ยนตัวแก้สมการ ผลต่างเล็กน้อยระหว่าง mean_cov กับ transpose จาก roundoff ถูกเฉลี่ยกลับให้สมมาตร
การทำงานได้ของสมการไม่ยืนยันว่า เป็นค่าความเสี่ยงจริง เราถือว่า covariance นี้ทราบแล้วเพื่อแยกบทเรียนเรื่องค่าเฉลี่ยออกมา แต่ในการใช้งาน covariance เองก็ต้องประมาณ และมีความคลาดเคลื่อนตาม บท Covariance Estimation
ถ้าเชื่อ Views มากขึ้นหรือน้อยลง
ขนาดของ วัดความคลาดเคลื่อน ไม่ใช่เปอร์เซ็นต์โอกาสที่ View จะถูกต้อง ถ้าเพิ่ม โดยคงข้อมูลอื่นไว้ เรากำลังบอกว่า Views มีข้อมูลแม่นยำน้อยลง หากลด เรากำลังเพิ่มความมั่นใจในข้อมูล Views
ทดลองคูณ ทั้งเมทริกซ์ด้วย 0, 0.1, 1, 10 และหนึ่งล้าน ค่า 0 หมายถึงถือว่าทั้งสองสมการของ Views แน่นอนในแบบจำลอง ตัวอย่างนี้ใช้ได้เพราะสองแถวของ ไม่ซ้ำกันและเมทริกซ์ ยังเป็น PD
confidence_rows = []
for multiplier in [0.0, 0.1, 1.0, 10.0, 1_000_000.0]:
mean, mean_cov, _ = bl_posterior(covariance, pi, P, Q, multiplier * omega, tau)
confidence_rows.append({"Omega multiplier": multiplier,
"C expected excess (%)": mean[2] * 100,
"A-B expected spread (%)": (P @ mean)[1] * 100,
"C mean uncertainty SD (%)": np.sqrt(max(mean_cov[2, 2], 0)) * 100})
confidence_table = pd.DataFrame(confidence_rows).set_index("Omega multiplier")
print(confidence_table.round(6).to_string())
exact_mean, exact_M, _ = bl_posterior(covariance, pi, P, Q, np.zeros((2, 2)), tau)
assert np.allclose(P @ exact_mean, Q)
assert np.allclose(P @ exact_M @ P.T, 0, atol=1e-12)
print("Uncertainty left outside the two exact views:", round(np.trace(exact_M), 8))
| ตัวคูณ Ω | ค่าเฉลี่ย C (%) | ส่วนต่างค่าเฉลี่ย A − B (จุดเปอร์เซ็นต์) |
|---|---|---|
| 0 | 2.500000 | 2.000000 |
| 0.1 | 2.431298 | 2.102541 |
| 1 | 2.145762 | 2.505742 |
| 10 | 1.890899 | 2.826199 |
| 1,000,000 | 1.837501 | 2.887499 |
เมื่อเพิ่มความคลาดเคลื่อนมาก ๆ ผลเข้าใกล้ prior ของ C ที่ 1.8375% และ prior spread ที่ 2.8875 จุดเปอร์เซ็นต์ ส่วนกรณีไม่ให้มีความคลาดเคลื่อน เราได้ และความไม่แน่นอนของสองค่าที่ Views ระบุเป็นศูนย์
แต่ ทั้งเมทริกซ์ยังไม่เป็นศูนย์: รู้ค่าเฉลี่ย C และส่วนต่าง A/B ไม่ได้ทำให้รู้ระดับค่าเฉลี่ย A กับ B แยกจากกันทั้งหมด ยังเหลือความไม่แน่นอนในทิศทางที่ Views ไม่ได้ระบุไว้ อีกทั้ง ยังมีอยู่ แม้สมมติว่า Views ของค่าเฉลี่ยแน่นอน ผลตอบแทนที่เกิดขึ้นจริงก็ไม่ได้ถูกบังคับให้ตรงกับค่าเฉลี่ยนั้น
ตารางนี้เป็นการเปลี่ยนความคลาดเคลื่อนของสอง Views พร้อมกันตามข้อมูลชุดเดียว ไม่ใช่ข้อพิสูจน์ว่าการปรับความมั่นใจของ View หนึ่งจะทำให้ทุกน้ำหนักเคลื่อนทางเดียวเสมอเมื่อมี Views หลายข้อสัมพันธ์กัน
ทำไมเปลี่ยน τ แล้วค่าเฉลี่ยอาจไม่เปลี่ยน
ถ้าคง ไว้ แต่เพิ่ม เรากำลังเพิ่มความไม่แน่นอนของ prior เมื่อเทียบกับ Views อย่างไรก็ตามกฎสาธิตที่ใช้สร้าง ข้างต้นก็คูณด้วย ด้วย หากสร้าง ใหม่ตามกฎนี้ทุกครั้ง ความไม่แน่นอนทั้งสองส่วนจะเพิ่มด้วยตัวคูณเดียวกัน
เขียน แล้วแทนลงใน gain จะได้
ตัดกันในสูตรค่าเฉลี่ย posterior แต่ ยังมีขนาดแปรตาม จึงไม่ตัดกันจาก predictive covariance ทดลองทั้งกรณีสร้าง ใหม่และคง เดิมไว้:
tau_rows = []
for tau_value in [0.01, 0.05, 0.20]:
scaled_omega = np.diag(np.diag(P @ (tau_value * covariance) @ P.T))
scaled_mean, scaled_M, scaled_predictive = bl_posterior(
covariance, pi, P, Q, scaled_omega, tau_value)
fixed_mean, _, _ = bl_posterior(covariance, pi, P, Q, omega, tau_value)
assert np.allclose(scaled_mean, bl_mean)
tau_rows.append({"Tau": tau_value,
"C mean, scaled Omega (%)": scaled_mean[2] * 100,
"C mean, fixed Omega (%)": fixed_mean[2] * 100,
"C mean uncertainty SD (%)": np.sqrt(scaled_M[2, 2]) * 100,
"C predictive return SD (%)": np.sqrt(scaled_predictive[2, 2]) * 100})
print(pd.DataFrame(tau_rows).set_index("Tau").round(4).to_string())
เมื่อสร้าง ใหม่ตาม , ค่าเฉลี่ย C อยู่ที่ประมาณ 2.1458% เท่าเดิมทั้งสามกรณี แต่ SD ของความไม่แน่นอนเกี่ยวกับค่าเฉลี่ย C เปลี่ยนจากประมาณ 0.7043 เป็น 1.5748 และ 3.1496 จุดเปอร์เซ็นต์ตามลำดับ ส่วนกรณีคง เดิม ค่าเฉลี่ย C เปลี่ยนจากประมาณ 1.9362% ไปเป็น 2.1458% และ 2.3516%
ดังนั้นประโยคว่า “ค่า tau ไม่มีผล” ต้องระบุว่าไม่มีผลต่อค่าเฉลี่ยภายใต้กฎการปรับ ที่ใช้อยู่เท่านั้น หากนำ predictive covariance ไปหาน้ำหนักพอร์ต ผลของ tau อาจยังปรากฏผ่านความเสี่ยง
Views สองข้ออาจมาจากข้อมูลชุดเดียวกัน
ผู้วิเคราะห์สองคนอาจใช้ประมาณการเศรษฐกิจชุดเดียวกันจนความผิดพลาดสัมพันธ์กัน หรือ Views คนละข้ออาจมาจากโมเดลตัวเดียวกัน การตั้ง เป็นแนวทแยงในทุกกรณีจะข้ามเรื่องนี้ไป
ตัวอย่างต่อไปคง variance ของความคลาดเคลื่อนแต่ละ View ไว้ แต่สมมติ correlation ของความคลาดเคลื่อนเท่ากับ 0.6 เราสร้างช่องนอกแนวทแยงด้วยสูตรเดียวกับ covariance ทั่วไป คือ correlation คูณ SD ทั้งสองตัว
view_error_correlation = 0.6
view_error_sd = np.sqrt(np.diag(omega))
omega_correlated = np.outer(view_error_sd, view_error_sd) * np.array(
[[1.0, view_error_correlation], [view_error_correlation, 1.0]])
correlated_mean, correlated_M, correlated_predictive = bl_posterior(
covariance, pi, P, Q, omega_correlated, tau)
print("Correlated view-error covariance:\n", np.round(omega_correlated, 8))
print(pd.DataFrame({"Independent errors (%)": bl_mean * 100,
"Correlated errors (%)": correlated_mean * 100}, index=assets).round(4).to_string())
ได้ covariance ของ view errors นอกแนวทแยงประมาณ 0.00058864 ซึ่งเป็นคนละค่ากับ 0.000175 ใน ผลค่าเฉลี่ย A/B/C เปลี่ยนเป็นประมาณ 6.0981%, 3.8588% และ 2.2570% ตามลำดับ
ผลนี้ไม่ควรถูกแปลว่า errors สัมพันธ์กันแล้วต้องเชื่อ Views น้อยลงทุกทิศทางเสมอไป ในตัวอย่าง surprise ของ C เป็นบวก แต่ surprise ของ A/B เป็นลบ เมื่อ errors มักเคลื่อนไปทางเดียวกันตามสมมติฐาน ส่วนต่างของสัญญาณจึงมีความไม่แน่นอนต่างจากกรณี errors อิสระ การรวมต้องใช้เมทริกซ์ทั้งหมด
การคัดลอก View เดิมไม่ได้สร้างหลักฐานใหม่
แยกออกมาเฉพาะ View ของ C เพื่อเห็นปัญหานี้ชัดขึ้น หากเพิ่มแถว C=2.5% เดิมอีกหนึ่งแถวแล้วตั้ง errors ให้เป็นอิสระ โปรแกรมจะเข้าใจว่าเราได้ข้อมูลใหม่อีกชิ้น ถ้าเป็นการคัดลอกข้อมูลเดิมจริง ต้องรวมเป็น View เดียว หรือระบุการพึ่งพากันให้ตรงความจริง
one_P, one_Q, one_omega = P[:1], Q[:1], omega[:1, :1]
one_mean, one_M, _ = bl_posterior(covariance, pi, one_P, one_Q, one_omega, tau)
duplicate_P = np.repeat(one_P, 2, axis=0)
duplicate_Q = np.repeat(one_Q, 2)
duplicate_rows = [{"Treatment": "One observation", "C mean (%)": one_mean[2] * 100,
"C mean uncertainty SD (%)": np.sqrt(one_M[2, 2]) * 100}]
for label, error_corr in [("Two independent observations", 0.0),
("Nearly identical errors", 0.999)]:
duplicate_omega = one_omega[0, 0] * np.array([[1.0, error_corr], [error_corr, 1.0]])
mean, mean_cov, _ = bl_posterior(covariance, pi, duplicate_P, duplicate_Q, duplicate_omega, tau)
duplicate_rows.append({"Treatment": label, "C mean (%)": mean[2] * 100,
"C mean uncertainty SD (%)": np.sqrt(mean_cov[2, 2]) * 100})
print(pd.DataFrame(duplicate_rows).set_index("Treatment").round(6).to_string())
try:
bl_posterior(covariance, pi, duplicate_P, duplicate_Q,
np.full((2, 2), one_omega[0, 0]), tau)
except np.linalg.LinAlgError:
print("Exactly duplicated information: keep one row instead of solving a singular system")
View ของ C เพียงข้อเดียวให้ค่าเฉลี่ย posterior C เท่ากับ 2.168750% และ SD ความไม่แน่นอนประมาณ 1.581139 จุดเปอร์เซ็นต์ หากป้อนซ้ำแต่บอกว่าเป็นข้อมูลอิสระ ค่าเฉลี่ยจะขยับไปหา 2.5% มากขึ้นเป็น 2.279167% และ SD ลดเหลือ 1.290994 จุดเปอร์เซ็นต์ การเปลี่ยนนี้สมเหตุผลเมื่อเป็นหลักฐานอิสระจริง แต่เป็นการนับข้อมูลซ้ำถ้าใช้รายงานเดิมสองครั้ง
เมื่อ errors มี correlation 0.999 ผลเกือบกลับมาตรงกับ View เดียว ส่วนกรณีสำเนาเหมือนกันทุกประการที่ correlation เป็น 1 ระบบในรูปนี้เป็น singular โค้ดจึงหยุดและแจ้งให้เก็บแถวเดียว try กับ except ในตัวอย่างใช้รับข้อผิดพลาดที่ตั้งใจสาธิต ไม่ได้ทำให้ระบบที่ไม่มีคำตอบเฉพาะตัวกลายเป็นระบบที่ใช้ได้
ค่าเฉลี่ยชุดเดียวกันยังเลือกพอร์ตได้หลายโจทย์
Black–Litterman ให้ค่าประมาณและความไม่แน่นอน ขั้นเลือกพอร์ตต้องระบุอีกว่าถือเงินสดได้หรือไม่ กู้ยืมได้หรือไม่ ขายชอร์ตได้หรือไม่ และใช้ covariance ใดวัดความเสี่ยง เราจะเทียบสองโจทย์โดยคงข้อมูล Views ชุดเดียวกัน
โจทย์แรก: ให้น้ำหนักสินทรัพย์เสี่ยงเปลี่ยนได้ และถือเงินสดส่วนที่เหลือ
ให้ เป็นความเกลียดความเสี่ยงของผู้เลือกพอร์ต และ เป็น covariance ที่เลือกใช้ ประโยชน์เชิงค่าเฉลี่ย–variance ของพอร์ตเขียนเป็น
เมื่อไม่มีข้อจำกัดน้ำหนักและ เป็น PD คำตอบได้จาก จึงใช้ solve อีกครั้ง น้ำหนักเงินสดคือ ค่าติดลบหมายถึงกู้ยืมที่อัตราปลอดความเสี่ยงเพื่อลงทุนเพิ่ม ส่วนสินทรัพย์ที่มีน้ำหนักติดลบหมายถึงขายชอร์ต สมมติฐานนี้ยังไม่รวมส่วนต่างอัตรากู้ยืม ต้นทุน หรือข้อจำกัดหลักประกัน
ในช่วงแรกกำหนด เพื่อเทียบกับ prior เดิม เราจะแยกสัญลักษณ์ และ เพราะการเปลี่ยนความเสี่ยงที่ผู้ลงทุนยอมรับกับการเปลี่ยนค่าที่ใช้สร้าง prior เป็นคนละการตัดสินใจ
ตรวจกรณีไม่มี Views ก่อน
เมื่อไม่มี Views, และ ถ้าใช้ จะได้น้ำหนัก กลับมา แต่ถ้าใช้ predictive covariance จะได้
no_view_mean, no_view_M, no_view_predictive = bl_posterior(
covariance, pi, np.empty((0, 3)), np.empty(0), np.empty((0, 0)), tau)
no_view_fixed_weights = np.linalg.solve(risk_aversion * covariance, no_view_mean)
no_view_predictive_weights = np.linalg.solve(risk_aversion * no_view_predictive, no_view_mean)
assert np.allclose(no_view_mean, pi)
assert np.allclose(no_view_M, tau * covariance)
assert np.allclose(no_view_fixed_weights, benchmark_weights)
assert np.allclose(no_view_predictive_weights, benchmark_weights / (1 + tau))
print("No views, fixed Sigma risky weights:", np.round(no_view_fixed_weights, 6))
print("No views, predictive risky weights:", np.round(no_view_predictive_weights, 6))
print(f"Predictive cash weight: {1 - no_view_predictive_weights.sum():.4%}")
np.empty((0, 3)) สร้างเมทริกซ์ที่ไม่มีแถว Views แต่ยังมีสามคอลัมน์สินทรัพย์ ไม่ใช่การใส่ View ว่าผลตอบแทนเป็นศูนย์ ฟังก์ชันจัดการกรณีนี้โดยคืน prior และความไม่แน่นอนเดิม
กรณี fixed ได้ A/B/C เป็น 50%/30%/20% ส่วน predictive covariance ได้ประมาณ 47.6190%/28.5714%/19.0476% รวมสินทรัพย์เสี่ยง 95.2381% และเงินสด 4.7619% อัตราส่วนระหว่างสินทรัพย์เสี่ยงยังเท่าเดิม แต่จำนวนเงินที่ลงทุนในสินทรัพย์เสี่ยงลดลง เพราะโจทย์นี้คำนึงถึงความไม่แน่นอนของค่าเฉลี่ยด้วย
ใส่ Views แล้วแก้โจทย์เดิม
fixed_sigma_weights = np.linalg.solve(risk_aversion * covariance, bl_mean)
predictive_weights = np.linalg.solve(risk_aversion * bl_predictive, bl_mean)
allocation_table = pd.DataFrame({
"Fixed Sigma (%)": np.r_[fixed_sigma_weights, 1 - fixed_sigma_weights.sum()] * 100,
"Predictive Sigma+M (%)": np.r_[predictive_weights, 1 - predictive_weights.sum()] * 100
}, index=assets + ["Cash"])
print(allocation_table.round(4).to_string())
| รายการ | ใช้ Σ คงเดิม (%) | ใช้ predictive Σ + M (%) |
|---|---|---|
| A | 44.7455 | 43.1351 |
| B | 35.2545 | 33.0554 |
| C | 34.1695 | 34.4473 |
| เงินสด | −14.1695 | −10.6378 |
ตัวอย่างนี้ทั้งสองกรณีต้องกู้ยืม หากผู้ลงทุนห้ามกู้ยืม คำตอบนี้ยังไม่ใช่พอร์ตที่ทำได้ตามข้อกำหนดของเขา และไม่ควรนำเฉพาะน้ำหนักสินทรัพย์เสี่ยงไปหารให้รวมหนึ่งแล้วเรียกว่าเป็นคำตอบของโจทย์ utility เดิม การเปลี่ยนขนาดพอร์ตเปลี่ยนทั้งผลตอบแทนและความเสี่ยงที่ฟังก์ชันประโยชน์กำลังชั่งน้ำหนัก
ในโค้ด np.r_[risky_weights, cash_weight] ต่อรายการน้ำหนักเงินสดไว้ท้ายเวกเตอร์เพื่อแสดงตารางเท่านั้น ไม่ได้ทำการปรับน้ำหนักที่แก้ได้
โจทย์ที่สอง: Long-only และลงทุนในสามสินทรัพย์เต็มจำนวน
เพิ่มข้อจำกัด และ โดยไม่มีเงินสดเป็นสินทรัพย์ในชุดนี้ เราต้องแก้โจทย์ใหม่:
def long_only_utility(mean, risk_cov, investor_aversion=2.5):
mean, risk_cov = np.asarray(mean, dtype=float), np.asarray(risk_cov, dtype=float)
if (mean.ndim != 1 or len(mean) == 0 or risk_cov.shape != (len(mean), len(mean))
or not np.isfinite(mean).all() or not np.isfinite(risk_cov).all()
or not np.isfinite(investor_aversion) or investor_aversion <= 0
or not np.allclose(risk_cov, risk_cov.T, atol=1e-12)):
raise ValueError("Need finite means, a symmetric covariance and positive risk aversion")
np.linalg.cholesky(risk_cov)
n = len(mean)
result = minimize(
lambda w: investor_aversion / 2 * (w @ risk_cov @ w) - w @ mean,
np.full(n, 1 / n), jac=lambda w: investor_aversion * risk_cov @ w - mean,
method="SLSQP", bounds=[(0, 1)] * n,
constraints={"type": "eq", "fun": lambda w: w.sum() - 1,
"jac": lambda w: np.ones(n)},
options={"ftol": 1e-12, "maxiter": 500})
if (not result.success or abs(result.x.sum() - 1) > 1e-8
or result.x.min() < -1e-8 or result.x.max() > 1 + 1e-8):
raise RuntimeError("Optimizer did not return a feasible solution")
return result.x
long_only_weights = long_only_utility(bl_mean, bl_predictive, risk_aversion)
print("Fully invested, long-only weights (%):", np.round(long_only_weights * 100, 4))
print(f"Cash weight: {1 - long_only_weights.sum():.4%}")
print(f"Model expected excess return: {long_only_weights @ bl_mean:.4%}")
print(f"Predictive SD: {np.sqrt(long_only_weights @ bl_predictive @ long_only_weights):.4%}")
SciPy minimize แบบ SLSQP หาค่าต่ำสุด เราจึงเปลี่ยนเครื่องหมายของ utility แล้วใส่เป็น objective ส่วน jac ให้ความชัน, bounds กำหนดช่วงน้ำหนัก และ constraints บังคับผลรวมหนึ่ง หลังจบยังตรวจสถานะโปรแกรมและความเป็นไปได้ของน้ำหนัก
ได้น้ำหนัก A/B/C ประมาณ 43.2221%/30.5768%/26.2011% ไม่มีเงินสดและไม่มีการกู้ยืม ผลตอบแทนส่วนเกินคาดหวังตามโมเดลประมาณ 4.3831% และ predictive SD ประมาณ 12.8762% ต่อปี ทั้งสองตัวเลขเป็นผลคำนวณจากสมมติฐาน ไม่ใช่ผลตอบแทนหรือความผันผวนที่ยืนยันจากข้อมูลทดสอบ
คำกล่าวว่าสินทรัพย์ที่ไม่มี Views จะมีน้ำหนักเท่า benchmark เสมอ จึงต้องระวัง ในสูตร unconstrained บางรูปสามารถแยกพอร์ตเป็นฐานอ้างอิงกับส่วนปรับตาม Views ได้ แต่เมื่อเปลี่ยนฐานความเสี่ยง ปรับขนาดให้ผลรวมหนึ่ง หรือเพิ่มข้อจำกัด น้ำหนักอื่นอาจเปลี่ยนด้วย ต้องระบุโจทย์ที่ใช้ก่อนอธิบายผล
เปลี่ยนความเกลียดความเสี่ยงของใคร
ถ้าเราเชื่อค่าเฉลี่ย posterior และ predictive covariance ชุดเดิม แต่เพิ่ม ของผู้ลงทุนในโจทย์ที่มีเงินสด เขาจะลงทุนในสินทรัพย์เสี่ยงน้อยลง สูตร ทำให้น้ำหนักสินทรัพย์เสี่ยงลดครึ่งหนึ่งเมื่อเพิ่ม เป็นสองเท่า โดยยังไม่เปลี่ยน หรือ Views
aversion_rows = []
for investor_aversion in [1.25, 2.5, 5.0]:
risky_weights = np.linalg.solve(investor_aversion * bl_predictive, bl_mean)
aversion_rows.append({"Investor risk aversion": investor_aversion,
"Risky allocation (%)": risky_weights.sum() * 100,
"Cash (%)": (1 - risky_weights.sum()) * 100,
"Model expected excess (%)": risky_weights @ bl_mean * 100})
print(pd.DataFrame(aversion_rows).set_index("Investor risk aversion").round(4).to_string())
new_prior_pi = 5.0 * covariance @ benchmark_weights
new_prior_mean, _, _ = bl_posterior(covariance, new_prior_pi, P, Q, omega, tau)
print("Different prior calibration, posterior excess (%):", np.round(new_prior_mean * 100, 4))
เมื่อ เท่ากับ 1.25, 2.5 และ 5 สัดส่วนสินทรัพย์เสี่ยงรวมประมาณ 221.2756%, 110.6378% และ 55.3189% ตามลำดับ ค่าแรกจึงต้องกู้ยืมมาก และอาจใช้ไม่ได้ภายใต้ข้อจำกัดจริง ความสัมพันธ์แบบหารตาม นี้เป็นของโจทย์ unconstrained ที่มีเงินสด ไม่ใช่คำตอบของโจทย์ fully invested ในส่วนก่อนหน้า
ท้ายโค้ดลองอีกการเปลี่ยนหนึ่ง: สร้าง prior ใหม่ด้วย แล้วคง Views ไว้ ค่าเฉลี่ย posterior กลายเป็นประมาณ 10.8871%, 7.0881% และ 3.0057% นี่คือการเปลี่ยนข้อมูลความเชื่อตั้งต้นเอง จึงไม่เหมือนการให้ผู้ลงทุนคนใหม่ถือพอร์ตจากค่าประมาณเดิม การทดลองความไวของแบบจำลองควรบอกให้ชัดว่าคงตัวแปรใดและเปลี่ยนตัวแปรใด
ลองตรวจความเข้าใจ
- ในตัวอย่างหนึ่งมิติ ถ้าเปลี่ยน SD ของความคลาดเคลื่อน View จาก 1 เป็น 2 จุดเปอร์เซ็นต์ โดย prior ยังเหมือนเดิม ค่าเฉลี่ย posterior และ SD ของความไม่แน่นอนเป็นเท่าไร?
- View A ชนะ B อยู่ 2 จุดเปอร์เซ็นต์เป็นการเพิ่มหรือลดส่วนต่างเมื่อเทียบกับ prior ของตัวอย่าง และทราบได้จากตัวเลขใด?
- หากความไม่แน่นอนของค่าเฉลี่ยสินทรัพย์หนึ่งมี variance เท่ากับ 0.0004 และ conditional return variance เท่ากับ 0.01, predictive SD เท่ากับ 2% หรือไม่?
- เพราะเหตุใดการเพิ่ม tau จึงอาจไม่เปลี่ยนค่าเฉลี่ย posterior แต่เปลี่ยนน้ำหนักที่ใช้ predictive covariance ได้?
- ถ้าคัดลอกรายงานประมาณการฉบับเดียวกันมาเป็นสอง Views จะใช้ แบบแนวทแยงและนับทั้งสองเป็นข้อมูลอิสระได้หรือไม่?
- กรณีไม่มี Views, , และใช้ predictive covariance โดยถือเงินสดได้ สัดส่วนสินทรัพย์เสี่ยงรวมกับเงินสดเป็นเท่าไร? ถ้าข้อกำหนดเปลี่ยนเป็นลงทุนเต็มจำนวนในสินทรัพย์เสี่ยง ต้องแก้โจทย์เดิมหรือโจทย์ใหม่?
แนวคำตอบ
- Prior และ View มี variance เท่ากัน จึงให้น้ำหนักครึ่งต่อครึ่ง ค่าเฉลี่ยเป็น 5% และ variance posterior เท่ากับ จึงมี SD ประมาณ 1.4142 จุดเปอร์เซ็นต์
- ลดส่วนต่าง เพราะ prior ให้ จุดเปอร์เซ็นต์ จึงมี surprise เท่ากับ −0.8875 จุดเปอร์เซ็นต์ คำว่า “ชนะ” เพียงอย่างเดียวไม่ได้บอกทิศทางการปรับจาก prior
- ไม่ใช่ 2% ตัวเลขนั้นเป็น SD ของความไม่แน่นอนเกี่ยวกับค่าเฉลี่ย predictive SD เท่ากับ
- หาก เพิ่มตาม tau ด้วย ตัวคูณจะตัดกันใน gain และค่าเฉลี่ย แต่ ยังเปลี่ยน จึงเปลี่ยน covariance ในโจทย์เลือกพอร์ตได้ ถ้าคง ไว้ ค่าเฉลี่ยก็อาจเปลี่ยนด้วย
- ไม่ได้ถ้าเป็นข้อมูลชิ้นเดียวกันจริง การนับเป็นอิสระทำให้ความมั่นใจเพิ่มโดยไม่มีหลักฐานใหม่ ควรเก็บ View เดียว หรือสร้างโครงสร้างความคลาดเคลื่อนที่สะท้อนข้อมูลร่วมกัน โดยตรวจระบบ singular ด้วย
- สินทรัพย์เสี่ยงรวม และเงินสดประมาณ 9.0909% การบังคับให้ลงทุนในสินทรัพย์เสี่ยงเต็มจำนวนเพิ่มข้อจำกัดใหม่ จึงต้องแก้โจทย์ตามข้อจำกัดนั้น ไม่ควรเรียกน้ำหนักที่ normalize แล้วว่าคำตอบ utility unconstrained เดิม
แหล่งที่มาและขอบเขต
อ่าน Transcript ภาษาอังกฤษเต็มของ Black-Litterman Analysis และ Lab Session — Black Litterman เมื่อ 3 ตุลาคม 2026 และตรวจ lab_23.ipynb ผ่าน Coursera Labs and code แบบอ่านอย่างเดียว โดยเฉพาะเซลล์ In[4]–In[6] สำหรับ implied returns, default Omega และ posterior; In[8] กับ In[22]–In[23] สำหรับการปรับสเกลน้ำหนักและฐาน ไม่ได้รันหรือแจกจ่าย Notebook ของคอร์ส
สูตร prior และ posterior ตรวจประกอบกับ He และ Litterman, The Intuition Behind Black-Litterman Model Portfolios, December 1999, Appendix B และ Idzorek, A Step-by-Step Guide to the Black-Litterman Model, ฉบับ July 2004, Section 2 ซึ่งเป็นสำเนางานผู้เขียนที่โฮสต์ในเว็บไซต์มหาวิทยาลัย เอกสาร PyPortfolioOpt แสดงสูตร predictive covariance เช่นกัน บทนี้เขียนสูตรและฟังก์ชันด้วย NumPy เอง ไม่ต้องติดตั้ง PyPortfolioOpt
โค้ด Lab เรียกเมทริกซ์ที่คืนว่า sigma_bl และคำนวณเป็น บทนี้จึงแยกชื่อความไม่แน่นอนของค่าเฉลี่ยออกจากความเสี่ยง predictive อย่างชัดเจน ส่วนค่า tau, covariance, Views และความคลาดเคลื่อนทั้งหมดเป็นข้อกำหนดสำหรับสอน ไม่ใช่ค่าที่สอบเทียบกับตลาดหรือความสามารถพยากรณ์จริง การใช้ข้อมูลจริงต้องตรวจหน่วย ช่วงเวลา ลำดับสินทรัพย์ แหล่งข้อมูลร่วมกันของ Views และผลนอกช่วงที่ใช้เลือกพารามิเตอร์