Advanced · Expected Returns · บทย่อย 1
Expected Return: ค่าเฉลี่ยที่เรายังไม่รู้
ถ้าผลตอบแทนย้อนหลังเฉลี่ย 1% ต่อเดือน เรามีเหตุผลมากแค่ไหนที่จะใส่ 1% เป็นผลตอบแทนคาดหวังของเดือนหน้า?
ในบทประมาณ Covariance เราดูความคลาดเคลื่อนของข้อมูลด้านความเสี่ยงมาแล้ว อีกข้อมูลหนึ่งที่ optimizer ต้องใช้คือ expected return หรือค่าเฉลี่ยของผลตอบแทนในแบบจำลองของอนาคต การคำนวณค่าเฉลี่ยย้อนหลังทำได้ด้วยการบวกและหาร แต่การนำค่าที่ได้ไปพยากรณ์ต้องเพิ่มสมมติฐานว่าประวัติช่วงนั้นบอกอะไรเกี่ยวกับอนาคตได้
บทนี้เริ่มจากผลตอบแทนหกเดือน คำนวณความไม่แน่นอนของค่าเฉลี่ย แล้วทดลองให้ความไม่แน่นอนนั้นส่งผ่านไปสู่น้ำหนักพอร์ต จากนั้นจึงใช้ shrinkage และ factor model เพื่อตั้งสมมติฐานที่มีโครงสร้างมากขึ้น เราจะระบุด้วยว่าสิ่งใดได้จากข้อมูล และสิ่งใดเป็นความเชื่อที่ใส่เพิ่ม
เนื้อหาประกอบหัวข้อการประมาณผลตอบแทนคาดหวังของคอร์ส Advanced Portfolio Construction and Analysis with Python ตัวเลขทั้งหมดเป็นข้อมูลสมมติหรือการจำลองที่กำหนด seed ไม่มีผลทดสอบหุ้นจริง โค้ด 12 ชุดใช้ NumPy, pandas และ SciPy รันตามลำดับใน Notebook ใหม่ได้ ตัวอย่างต้นบทเป็นรายเดือน ช่วงจัดพอร์ตใช้ค่ารายปี แล้วกลับมาใช้รายเดือนในตัวอย่าง factor โดยประกาศหน่วยใหม่ทุกครั้ง
ค่าเฉลี่ยย้อนหลังเป็นค่าที่คำนวณได้ ส่วนค่าเฉลี่ยอนาคตต้องประมาณ
ให้ เป็นผลตอบแทนของเดือนที่ หน่วยทศนิยม เช่น หมายถึง 2% ถ้ามีข้อมูล เดือน ค่าเฉลี่ยเลขคณิตของชุดข้อมูลคือ
เครื่องหมายขีดด้านบนบอกว่าเรากำลังเฉลี่ยข้อมูลที่มีอยู่ ส่วน ใช้แทนผลตอบแทนคาดหวังของช่วงถัดไปในแบบจำลอง เมื่อแบบจำลองมีหลายผลลัพธ์ ค่า คือค่าเฉลี่ยถ่วงน้ำหนักด้วยความน่าจะเป็นของผลลัพธ์เหล่านั้น เดือนหน้าที่เกิดขึ้นจริงอาจอยู่ไกลจาก ได้
สมมติเราบันทึกผลตอบแทนรวมของสินทรัพย์หนึ่งเป็น −4%, 2%, 5%, −1%, 3% และ 1% ผลบวกเท่ากับ 6% แล้วหารหกจึงได้ค่าเฉลี่ย 1% ต่อเดือน คำว่า “รวม” หมายถึงผลตอบแทนที่นับกระแสเงินสดจากสินทรัพย์ตาม convention เดียวกัน ไม่ปนราคากับผลตอบแทน
import ... as ... โหลดไลบรารีและตั้งชื่อย่อ np สำหรับ NumPy และ pd สำหรับ pandas np.array เก็บตัวเลขเป็นชุด len นับจำนวนสมาชิก .sum() บวกสมาชิกทั้งหมด และ np.prod คูณสมาชิกทั้งหมด ** เป็นการยกกำลัง ส่วน :.4% แสดงทศนิยมเป็นเปอร์เซ็นต์สี่ตำแหน่งโดยไม่เปลี่ยนค่าที่เก็บไว้
import numpy as np
import pandas as pd
mean_sample = np.array([-0.04, 0.02, 0.05, -0.01, 0.03, 0.01])
sample_size = len(mean_sample)
sample_mean = mean_sample.sum() / sample_size
sample_total = np.prod(1 + mean_sample) - 1
sample_geometric = (1 + sample_total) ** (1 / sample_size) - 1
print(f"Arithmetic mean per month: {sample_mean:.4%}")
print(f"Six-month total return: {sample_total:.4%}")
print(f"Geometric mean per month: {sample_geometric:.4%}")
ได้ค่าเฉลี่ยเลขคณิต 1.0000% ต่อเดือน ผลตอบแทนสะสมหกเดือน 5.8899% และค่าเฉลี่ยเรขาคณิต 0.9584% ต่อเดือน ตัวเลขสุดท้ายคืออัตราคงที่ที่ทบต้นหกครั้งแล้วได้มูลค่าปลายงวดเท่ากัน ดูวิธีทบต้นเพิ่มเติมในบท Returns
ทั้งสามค่าตอบคนละคำถาม ผลตอบแทนสะสมบอกการเปลี่ยนมูลค่าตลอดช่วง ค่าเฉลี่ยเรขาคณิตสรุปอัตราทบต้น ส่วนค่าเฉลี่ยเลขคณิตเป็นตัวประมาณที่ใช้บ่อยสำหรับค่าเฉลี่ยหนึ่งช่วงเวลา การใช้ ประมาณ ต้องอาศัยสมมติฐานเกี่ยวกับการสุ่มและความคงที่ของกระบวนการผลตอบแทน
ตัวอย่างข้อมูลหกเดือนนี้มีไว้ตรวจเลข จึงยังไม่มีหลักฐานว่ามาจากกระบวนการใด แม้คำนวณค่าเฉลี่ยได้ถูกต้อง เราก็ยังไม่รู้จากตัวเลขหกตัวเพียงอย่างเดียวว่า expected return ของเดือนหน้าคือ 1%
Standard error วัดความไม่แน่นอนของค่าเฉลี่ย
Standard deviation หรือ SD ของผลตอบแทนบอกการกระจายของผลตอบแทนแต่ละช่วง ส่วน standard error หรือ SE ของค่าเฉลี่ยบอกว่า หากสุ่มประวัติใหม่หลายชุด ค่าเฉลี่ยของแต่ละชุดจะกระจายกันเท่าไร
เริ่มจากสมมติฐาน IID: ผลตอบแทนแต่ละช่วงเป็นอิสระจากกันและมาจากการแจกแจงเดียวกัน มีค่าเฉลี่ย และ variance คงที่ เราจึงบวก variance ของแต่ละช่วงได้:
การหารด้วย ในค่าเฉลี่ยกลายเป็นการหารด้วย เมื่อคิด variance แล้วผลบวกมี พจน์ จึงเหลือตัวหาร ก่อนถอดราก เรามักไม่รู้ จึงแทนด้วย sample SD:
สำหรับข้อมูลหกเดือน ผลรวม squared deviations เท่ากับ 0.005 หารด้วย ได้ sample variance 0.001 จึงได้ หรือ 3.1623% ต่อเดือน และ SE ของค่าเฉลี่ยเท่ากับ หรือ 1.2910 จุดเปอร์เซ็นต์ต่อเดือน
ช่วงความเชื่อมั่นของค่าเฉลี่ย
ถ้าเพิ่มสมมติฐานว่า observations เป็น IID Normal ช่วงความเชื่อมั่น 95% ของ เมื่อไม่รู้ variance คือ
เป็นค่าขอบของการแจกแจง Student's t ที่มีพื้นที่ด้านซ้าย 97.5% เหลือด้านขวา 2.5% และใช้ขอบล่างอีก 2.5% รวมเป็นพื้นที่หาง 5% stats.t.ppf คืนค่าขอบนี้ ส่วน df คือ degrees of freedom ซึ่งในสูตรนี้เท่ากับ เอกสาร NIST อธิบายสูตรและความหมายของช่วงความเชื่อมั่นนี้
from scipy import stats
sample_sd = np.sqrt(np.sum((mean_sample - sample_mean) ** 2) / (sample_size - 1))
mean_se = sample_sd / np.sqrt(sample_size)
t_critical = stats.t.ppf(0.975, df=sample_size - 1)
mean_ci = sample_mean + np.array([-1, 1]) * t_critical * mean_se
print(f"Monthly return SD: {sample_sd:.4%}")
print(f"SE of monthly mean: {mean_se:.4%}")
print(f"t critical value: {t_critical:.4f}")
print("95% mean CI (% per month):", np.round(100 * mean_ci, 4))
t_critical ประมาณ 2.5706 ช่วงที่คำนวณได้คือ −2.3186% ถึง 4.3186% ต่อเดือน ภายใต้สมมติฐานที่กำหนด ข้อมูลชุดนี้ยังแยกค่าเฉลี่ยบวกเล็กน้อยออกจากศูนย์หรือค่าเฉลี่ยติดลบบางค่าได้ไม่ชัด ค่า SE ที่ใหญ่กว่าค่าเฉลี่ย 1% ช่วยให้เห็นขนาดความไม่แน่นอนนั้น
ความเชื่อมั่น 95% เป็นคุณสมบัติของวิธีสร้างช่วงเมื่อสุ่มข้อมูลซ้ำ หากเงื่อนไขของวิธีเป็นจริง ช่วงประมาณ 95% จะครอบคลุม ที่คงที่ ช่วงที่คำนวณจากข้อมูลชุดหนึ่งแล้วไม่ได้แปลว่าเดือนหน้าจะอยู่ในช่วงนั้นด้วยโอกาส 95% การทำนายผลตอบแทนหนึ่งเดือนยังต้องรับความผันผวนของเดือนใหม่เพิ่มเติม
ในข้อมูลตลาด การมี serial correlation หรือพารามิเตอร์เปลี่ยนตามเวลาอาจทำให้สูตร บอกความไม่แน่นอนผิดขนาด ผลตอบแทนหางหนาและข้อมูลน้อยยังทำให้การใช้ t interval ตามตัวอย่างไม่แม่น ต้องเลือกวิธีอนุมานที่รองรับสมมติฐานของข้อมูลก่อนตีความช่วงที่โปรแกรมคืนมา
เพิ่มความถี่ข้อมูลกับเพิ่มจำนวนปีให้ข้อมูลคนละแบบ
ลองสมมติแบบจำลองผลตอบแทนแบบบวกที่มี mean และ variance คงที่ มีช่วงย่อย ช่วงต่อปี และใช้ข้อมูลย้อนหลัง ปี จึงมี observations ภายใต้ IID ให้ เป็น SD ของผลรวมผลตอบแทนหนึ่งปี จะได้
เมื่อคูณค่าเฉลี่ยด้วย ค่า SE ต้องคูณ เช่นกัน:
pd.DataFrame จัดผลลัพธ์เป็นตาราง โดยชื่อใน dictionary เป็นชื่อคอลัมน์และ index เป็นป้ายแถว เราเก็บการคำนวณเป็นทศนิยม แล้วคูณ 100 เฉพาะคอลัมน์ที่ต้องการแสดงเป็นเปอร์เซ็นต์
ดังนั้น ถ้า annual SD เท่ากับ 20% และมีประวัติห้าปี ค่า SE ของ annual mean เท่ากับ จุดเปอร์เซ็นต์ ทั้งกรณีข้อมูลเดือนละหนึ่งแถวและกรณีปีละ 252 วัน ภายใต้แบบจำลองเดียวกัน การเปลี่ยนความถี่ทำให้ SD ต่อแถวและจำนวนแถวเปลี่ยนชดเชยกัน
annual_sigma = 0.20
history_years = 5
periods_per_year = np.array([12, 252])
observations = history_years * periods_per_year
period_sd = annual_sigma / np.sqrt(periods_per_year)
annual_mean_se = periods_per_year * period_sd / np.sqrt(observations)
frequency_table = pd.DataFrame({
"observations": observations,
"period_SD_pct": 100 * period_sd,
"annual_mean_SE_pct": 100 * annual_mean_se,
}, index=["monthly", "daily"])
print(frequency_table.round(4))
years_grid = np.array([1, 4, 16, 64])
annual_se_by_year = annual_sigma / np.sqrt(years_grid)
print("Annual mean SE for 1, 4, 16, 64 years (%):", np.round(100 * annual_se_by_year, 4))
ตารางมี 60 เดือนหรือ 1,260 วัน SD ต่อเดือนประมาณ 5.7735% และ SD ต่อวันประมาณ 1.2599% แต่ SE ของ annual mean เท่ากัน ประวัติ 1, 4, 16 และ 64 ปีให้ SE เท่ากับ 20, 10, 5 และ 2.5 จุดเปอร์เซ็นต์ตามลำดับ การลด SE ลงครึ่งหนึ่งจึงต้องเพิ่มความยาวประวัติสี่เท่าในแบบจำลองนี้
หน่วยรายปีตอนนี้เป็น annualized arithmetic mean และ variance ของผลรวมแบบบวก ไม่ใช่ค่าเฉลี่ยหรือ variance ของผลตอบแทนที่ทบต้นจริงตลอดปี สมการนี้ใช้สอนว่าการประมาณ drift ขึ้นกับช่วงเวลาที่สังเกตอย่างไร ไม่ใช่สูตรที่แทนการทบต้นได้ทุกกรณี
ในทางปฏิบัติ ข้อมูลถี่อาจช่วยประมาณรายละเอียดความผันผวน แต่มีเรื่องราคาที่ไม่พร้อมกันและความสัมพันธ์ข้ามเวลาเพิ่มมา ส่วนข้อมูลหลายสิบปีอาจครอบคลุมธุรกิจ โครงสร้างตลาด หรือนโยบายที่เปลี่ยนไป การเพิ่ม จึงไม่ได้รับประกันว่าทุกปีมาจากการแจกแจงเดียวกัน
สุ่มประวัติหลายชุดเพื่อเห็นค่าเฉลี่ยที่แกว่งไปมา
ในการจำลอง เราตั้งค่าจริงของแบบจำลองเองได้ กำหนด mean รายเดือน 0.5%, SD รายเดือน 4% และผลตอบแทน IID Normal แต่ละประวัติมี 60 เดือน แล้วสุ่มประวัติ 10,000 ชุด ทุกชุดมาจาก mean เดียวกัน เราจะดูว่าค่าเฉลี่ยย้อนหลังต่างกันได้เท่าไร
default_rng สร้างตัวสุ่มและ seed ทำให้รันซ้ำได้ size=(10000, 60) สร้างตารางหนึ่งแถวต่อหนึ่งประวัติ .mean(axis=1) เฉลี่ยตามแนวคอลัมน์ภายในแต่ละแถว ddof=1 ใช้ตัวหาร 59 สำหรับ sample SD ส่วนการเฉลี่ยค่าจริง/เท็จจะให้สัดส่วนของแถวที่เป็นจริง
mean_rng = np.random.default_rng(20261003)
sim_true_mean, sim_true_sd = 0.005, 0.04
sim_months, sim_repeats = 60, 10000
sim_samples = mean_rng.normal(sim_true_mean, sim_true_sd, size=(sim_repeats, sim_months))
sim_means = sim_samples.mean(axis=1)
sim_ses = sim_samples.std(axis=1, ddof=1) / np.sqrt(sim_months)
sim_halfwidths = stats.t.ppf(0.975, sim_months - 1) * sim_ses
sim_coverage = np.mean(np.abs(sim_means - sim_true_mean) <= sim_halfwidths)
print(f"Theoretical SE: {sim_true_sd / np.sqrt(sim_months):.4%}")
print(f"SD of simulated sample means: {sim_means.std(ddof=1):.4%}")
print(f"Share of negative sample means: {np.mean(sim_means < 0):.2%}")
print(f"Share of intervals covering true mean: {sim_coverage:.2%}")
ตามทฤษฎี SE เท่ากับ จุดเปอร์เซ็นต์ ค่า SD ของ sample means ที่สุ่มได้ประมาณ 0.5057 จุดเปอร์เซ็นต์ มีค่าเฉลี่ยย้อนหลังติดลบประมาณ 16.62% ของประวัติ ทั้งที่กำหนด mean จริงเป็นบวก 0.5% ต่อเดือน
ช่วงความเชื่อมั่นจากรอบนี้ครอบคลุม mean จริงประมาณ 95.71% ค่านี้แกว่งตาม seed และจำนวนรอบ จึงไม่ต้องตรง 95.00% ทุกการจำลอง และยังเป็นผลภายใต้โลก IID Normal ที่เราสร้างเท่านั้น การจำลองจำนวนมากช่วยตรวจพฤติกรรมของวิธีภายใต้สมมติฐาน ไม่ได้ตรวจว่าตลาดทำตามสมมติฐานนั้น
Normal ในที่นี้เป็นแบบจำลองเพื่อศึกษา sample mean ไม่ได้นำไปสร้างราคาสินทรัพย์ สมมติฐาน Normal ของ simple return มีหางต่ำกว่า −100% ทางคณิตศาสตร์ จึงควรเลือกแบบจำลองที่เหมาะกับราคาหากเปลี่ยนโจทย์ไปจำลองความมั่งคั่ง
ค่าเฉลี่ยต่างกันเล็กน้อยอาจทำให้น้ำหนักต่างกันมาก
ต่อไปเปลี่ยนเป็นข้อมูลรายปีของสินทรัพย์สองตัว A และ B กำหนด SD เท่ากันที่ 20% และ correlation 0.995 เมื่อผลตอบแทนสองตัวเกือบเคลื่อนไหวด้วยกัน การเพิ่ม A พร้อมลด B อาจเปลี่ยนความเสี่ยงรวมไม่มากนัก optimizer จึงตอบสนองแรงต่อความต่างของ expected returns
ใช้โจทย์ mean–variance ที่ให้คะแนนพอร์ตเป็น
พจน์แรกเป็น expected return พจน์หลังหักคะแนนตาม variance และ กำหนดน้ำหนักที่ให้กับการลด variance ตัวอย่างเลือก โดยใช้ returns หน่วยทศนิยมและ covariance หน่วยทศนิยมยกกำลังสองรายปี ไม่มีต้นทุน ภาษี หรือข้อจำกัดขนาดสถานะในกรณีแรก
สำหรับสองสินทรัพย์ แทน แล้วหาจุดสูงสุดของฟังก์ชันกำลังสอง ได้
ส่วน คือ variance ของผลต่าง สูตรต้องมีตัวหารเป็นบวก ถ้าสองสินทรัพย์มี SD เดียวกัน สูตรย่อเหลือ เมื่อ ใกล้หนึ่ง ตัวหารเล็กลง
def ประกาศฟังก์ชันที่เรียกซ้ำได้ return ส่งน้ำหนักกลับมา long_only=False เป็นค่าเริ่มต้นที่อนุญาตน้ำหนักติดลบ หากตั้งเป็น True ฟังก์ชันใช้ np.clip จำกัดน้ำหนัก A ให้อยู่ในช่วง 0 ถึง 1 วิธี clip นี้แก้ปัญหา long-only ได้ตรงสำหรับโจทย์สองสินทรัพย์ฟังก์ชันเว้าแบบนี้ ไม่ใช่วิธีทั่วไปสำหรับพอร์ตหลายสินทรัพย์
def two_asset_mvo_weights(mu, covariance, gamma=3.0, long_only=False):
difference_variance = covariance[0, 0] + covariance[1, 1] - 2 * covariance[0, 1]
weight_a = (mu[0] - mu[1] + gamma * (covariance[1, 1] - covariance[0, 1]))
weight_a = weight_a / (gamma * difference_variance)
if long_only:
weight_a = np.clip(weight_a, 0, 1)
return np.array([weight_a, 1 - weight_a])
sensitivity_cov = 0.20 ** 2 * np.array([[1, 0.995], [0.995, 1]])
sensitivity_means = np.array([[0.07, 0.07], [0.0706, 0.0694], [0.0694, 0.0706]])
sensitivity_weights = np.array([two_asset_mvo_weights(mu, sensitivity_cov) for mu in sensitivity_means])
sensitivity_long_only = np.array([two_asset_mvo_weights(mu, sensitivity_cov, long_only=True) for mu in sensitivity_means])
print("Unconstrained A/B weights (%):\n", np.round(100 * sensitivity_weights, 2))
print("Long-only A/B weights (%):\n", np.round(100 * sensitivity_long_only, 2))
เมื่อ expected returns เป็น 7% ทั้งคู่ น้ำหนักเท่ากับ 50/50 เปลี่ยน A เป็น 7.06% และ B เป็น 6.94% จึงเปลี่ยนค่าที่ใส่ให้แต่ละตัวเพียง 0.06 จุดเปอร์เซ็นต์ แต่คำตอบที่อนุญาต short กลายเป็น 150/−50 เมื่อสลับค่าคาดหวังคำตอบก็กลายเป็น −50/150
น้ำหนัก 150/−50 หมายถึงถือ A มูลค่า 150% ของเงินพอร์ตและ short B มูลค่า 50% เมื่อตั้ง long-only ได้ 100/0 หรือ 0/100 แทน ข้อจำกัดตัดสถานะ short ออก แต่น้ำหนักยังเปลี่ยนจากมุมหนึ่งไปอีกมุมหนึ่งได้
ตัวอย่างนี้ตรึง covariance ไว้เพื่อแยกผลของ mean error ค่า ที่ต่างกันเกิดจากสมมติฐานของเรา ไม่ใช่หลักฐานว่า A หรือ B ให้ผลตอบแทนดีกว่าจริง การที่โปรแกรมหาคำตอบถูกต้องสำหรับข้อมูลเข้าจึงเป็นคนละเรื่องกับการประมาณข้อมูลเข้าได้แม่น
ลดความต่างของค่าประมาณด้วย Mean shrinkage
Mean shrinkage ผสมค่าประมาณจากข้อมูลกับ target ที่กำหนดไว้ ให้ เป็น target ของสินทรัพย์ และ เป็นสัดส่วนที่ให้น้ำหนัก target:
เมื่อ ใช้ค่าประมาณเดิมทั้งหมด เมื่อ ใช้ target ทั้งหมด สมมติค่าประมาณเดิมเป็น 7.06% และ 6.94% แล้วเลือก target ร่วมเป็นค่าเฉลี่ยข้ามสินทรัพย์สองตัว คือ 7% ค่า จึงให้ A เท่ากับ และ B เท่ากับ 6.97%
ในโค้ด [:, None] เพิ่มแนวคอลัมน์ให้ตัวเลข แต่ละค่าคูณ expected returns ทั้งสองตัวได้ เราใช้ฟังก์ชันจัดพอร์ตเดิมโดยตรึง covariance และ แล้วเปลี่ยนเฉพาะ expected returns
shrinkage_deltas = np.array([0.0, 0.5, 0.9, 1.0])
mean_target = sensitivity_means[1].mean()
shrunken_means = ((1 - shrinkage_deltas[:, None]) * sensitivity_means[1]
+ shrinkage_deltas[:, None] * mean_target)
shrunken_weights = np.array([two_asset_mvo_weights(mu, sensitivity_cov) for mu in shrunken_means])
shrinkage_results = pd.DataFrame({
"delta": shrinkage_deltas,
"mean_A_pct": 100 * shrunken_means[:, 0],
"mean_B_pct": 100 * shrunken_means[:, 1],
"weight_A_pct": 100 * shrunken_weights[:, 0],
"weight_B_pct": 100 * shrunken_weights[:, 1],
})
print(shrinkage_results.round(4).to_string(index=False))
| น้ำหนัก target | Expected A ต่อปี | Expected B ต่อปี | น้ำหนัก A | น้ำหนัก B |
|---|---|---|---|---|
| 0 | 7.060% | 6.940% | 150% | −50% |
| 0.5 | 7.030% | 6.970% | 100% | 0% |
| 0.9 | 7.006% | 6.994% | 60% | 40% |
| 1 | 7.000% | 7.000% | 50% | 50% |
การหด mean เข้าหากันลดความสุดโต่งของน้ำหนักในตัวอย่าง แต่ถ้า A มี expected return สูงกว่า B จริงมาก การหดแรงอาจลบข้อมูลที่มีประโยชน์ด้วย เราแลกความแตกต่างของค่าประมาณที่ลดลงกับความเสี่ยงที่จะดึงไปหา target ผิด ค่า ในตารางเป็นค่าทดลองเพื่อเห็นผลของสูตร ยังไม่ได้เลือกจากการทดสอบนอกช่วงประมาณ
Target ที่เป็นค่าเฉลี่ยข้ามสินทรัพย์มาจากข้อมูลชุดเดียวกัน จึงต้องนับความไม่แน่นอนของ target ด้วย การเรียก target ว่า “prior” ไม่ได้ทำให้มันเป็นข้อมูลจากภายนอก และการเฉลี่ยเข้าหา target เพียงอย่างเดียวยังไม่ได้ระบุ Bayesian model ครบ
Prior และ posterior เริ่มจากความไม่แน่นอนของพารามิเตอร์
Prior ใน Bayesian model คือการแจกแจงที่กำหนดให้พารามิเตอร์ก่อนนำข้อมูลชุดที่กำลังวิเคราะห์มาปรับความเชื่อ ส่วน likelihood บอกว่าข้อมูลที่เห็นสอดคล้องกับพารามิเตอร์แต่ละค่าเพียงใดภายใต้แบบจำลองการเกิดข้อมูล เมื่อนำทั้งสองส่วนมารวมตามกฎของ Bayes ได้ posterior ซึ่งเป็นการแจกแจงของพารามิเตอร์หลังรับข้อมูล
ลองใช้เหรียญเพื่อแยกขั้นตอนนี้ออกจากเรื่องพอร์ต ให้ เป็นโอกาสออกหัวที่ยังไม่รู้ กำหนด prior การแจกแจง Beta อยู่ระหว่างศูนย์กับหนึ่ง จึงใช้กับความน่าจะเป็นได้ ค่าเฉลี่ย prior นี้คือ และยังยอมให้ ต่างจาก 50% ได้
ภายใต้การโยนอิสระที่ใช้ เดียวกัน หากเห็นหัว ครั้งและก้อย ครั้ง posterior เป็น และค่าเฉลี่ย posterior เป็น
เมื่อเห็นหัวหก ก้อยสี่ ค่าเฉลี่ย posterior เป็น อยู่ระหว่าง prior mean 50% กับสัดส่วนหัวในข้อมูล 60% ถ้าเริ่มจาก prior เดิมแล้วมีข้อมูลหัว 600 ก้อย 400 แทน จะได้ ข้อมูลที่มากขึ้นให้น้ำหนักต่อ posterior มากขึ้นในแบบจำลองนี้ ไม่ต้องรอให้ข้ามเกณฑ์ใดก่อนจึงเริ่มอัปเดต สูตรและการอ่าน posterior mean เป็นค่าเฉลี่ยถ่วงน้ำหนักดูได้ใน Stanford STATS 200: Prior distributions
การประมาณ mean ของผลตอบแทนต้องเลือก likelihood และ prior ที่เหมาะกับข้อมูลต่อเนื่องต่างออกไป ตัวอย่างเหรียญใช้สอนกระบวนการอัปเดตเท่านั้น ส่วนวิธี frequentist ที่ใช้ SE ก่อนหน้าก็มีสมมติฐาน เช่น IID และการแจกแจงข้อมูล ทั้งสองแนวทางต้องประกาศสมมติฐานของตนให้ครบ
ถ้าไม่แยก Expected returns ระหว่างสินทรัพย์ จะได้พอร์ตอะไร?
พอร์ต maximum Sharpe ratio หรือ MSR คือพอร์ตที่ให้ Sharpe สูงสุดในชุดทางเลือกที่กำหนด คำว่า agnostic prior ในหัวข้อนี้หมายถึงการกำหนดโครงสร้างโดยไม่พยายามจัดอันดับ expected returns รายสินทรัพย์จากค่าเฉลี่ยระยะสั้น ยังต้องบอกให้ได้ว่าเราเลือกสมมติฐานแบบใด เพราะ equal means, equal Sharpe และ equal weights เป็นคนละข้อกำหนด
ให้สินทรัพย์ทุกตัวมี expected return เท่ากับ ใช้อัตราปลอดความเสี่ยง เดียวกัน และพอร์ตลงทุนเต็มจำนวน จะได้
ถ้า ตัวเศษเป็นค่าบวกเดียวกันทุกพอร์ต การเพิ่ม Sharpe ratio จึงเท่ากับลด volatility ภายใต้ชุดข้อจำกัดเดียวกัน คำตอบคือ global minimum variance หรือ GMV ในชุดพอร์ตนั้น หาก ทุกพอร์ตที่ volatility บวกมี Sharpe ศูนย์เท่ากัน และถ้า การหารด้วย volatility ที่มากขึ้นกลับทำให้ Sharpe ติดลบน้อยลง
ตัวอย่างใหม่มีสินทรัพย์สามตัว ใช้ covariance รายปีที่ให้ SD เท่ากับ 10%, 15% และ 20% เมทริกซ์ตัวอย่างนี้เป็น positive definite จึงหา GMV ที่ไม่มีข้อจำกัดเพิ่มเติมได้ด้วย ในโค้ด np.linalg.solve แก้สมการ แล้วหารทุกน้ำหนักด้วยผลรวม คำตอบของเมทริกซ์นี้เป็นบวกทั้งหมด จึงเป็น GMV ภายใต้ long-only ด้วย
expected_cov = np.array([[0.0100, 0.0020, 0.0010],
[0.0020, 0.0225, 0.0030],
[0.0010, 0.0030, 0.0400]])
ones = np.ones(3)
gmv_direction = np.linalg.solve(expected_cov, ones)
gmv_weights = gmv_direction / gmv_direction.sum()
comparison_weights = np.array([gmv_weights, ones / 3, [0.0, 0.0, 1.0]])
comparison_vols = np.array([np.sqrt(w @ expected_cov @ w) for w in comparison_weights])
common_premiums = np.array([0.04, 0.0, -0.04])
premium_edge_sharpes = common_premiums[None, :] / comparison_vols[:, None]
print("GMV weights (%):", np.round(100 * gmv_weights, 4))
print("Annual SD of GMV, EW, C (%):", np.round(100 * comparison_vols, 4))
print(pd.DataFrame(premium_edge_sharpes, index=["GMV", "EW", "C"],
columns=["premium +4%", "premium 0%", "premium -4%"]).round(4))
ได้ GMV ประมาณ 62.8943%, 23.1861%, 13.9196% และ annual volatility 8.3020% ส่วน equal-weight มี volatility 9.6896% และถือ C ตัวเดียวมี volatility 20% จะเห็นว่า equal expected returns ให้ GMV ที่น้ำหนักไม่เท่ากันในกรณีนี้
เมื่อ common excess return เป็น +4% ต่อปี Sharpe ของ GMV ประมาณ 0.4818 สูงกว่า equal-weight 0.4128 และ C 0.2 เมื่อเปลี่ยน common premium เป็น −4% ค่า Sharpe เป็น −0.4818, −0.4128 และ −0.2 ตามลำดับ ลำดับของพอร์ตกลับด้าน จึงต้องระบุเงื่อนไข premium เป็นบวกเมื่อนำ equal means ไปอธิบาย MSR
นี่เป็นการเปรียบเทียบพอร์ตสินทรัพย์เสี่ยงที่ลงทุนเต็มจำนวน การเพิ่มเงินสดหรือข้อจำกัดอื่นเปลี่ยนชุดทางเลือกด้วย และพอร์ตเงินสดที่ variance ศูนย์ไม่สามารถแทนในสูตร Sharpe นี้โดยหารด้วยศูนย์
Equal Sharpe นำไปสู่ Diversification ratio
อีกสมมติฐานคือให้ทุกสินทรัพย์มี Sharpe ratio รายตัวเท่ากับ จึงตั้ง expected excess return ให้แปรตาม SD:
สำหรับพอร์ต long-only ที่น้ำหนักรวมหนึ่ง จะได้
หรือ diversification ratio เปรียบเทียบค่าเฉลี่ยถ่วงน้ำหนักของ SD รายสินทรัพย์กับ SD ของพอร์ตรวม ตัวเศษไม่ใช่ volatility ของพอร์ต เพราะยังไม่รวมความสัมพันธ์ระหว่างสินทรัพย์ ถ้าการรวมสินทรัพย์ลด volatility ลงเมื่อเทียบกับตัวเศษ อัตราส่วนนี้จะสูงขึ้น
ใช้ covariance สามสินทรัพย์เดิมและตั้ง common Sharpe รายปีเท่ากับ 0.4 Expected excess returns จึงเป็น 4%, 6% และ 8% ต่อปี การคูณ DR ด้วยค่าบวกคงที่ 0.4 ไม่เปลี่ยนพอร์ตที่ให้ค่าสูงสุด เราจึงหา maximum-DR portfolio ผ่านการ maximize Sharpe ภายใต้สมมติฐานนี้ได้
minimize ของ SciPy หาค่าต่ำสุด เราจึงใส่เครื่องหมายลบหน้าอัตราส่วน lambda w: เป็นวิธีเขียนฟังก์ชันสั้นที่รับน้ำหนัก w เครื่องหมาย @ คูณเวกเตอร์หรือเมทริกซ์ ส่วน bounds กำหนดทุกน้ำหนักให้อยู่ระหว่าง 0 กับ 1 และ constraint ชนิด eq บังคับให้ w.sum() - 1 เท่ากับศูนย์
from scipy.optimize import minimize
def maximize_premium_ratio(premium, covariance):
count = len(premium)
result = minimize(
lambda w: -(w @ premium) / np.sqrt(w @ covariance @ w),
np.full(count, 1 / count), method="SLSQP",
bounds=[(0.0, 1.0)] * count,
constraints={"type": "eq", "fun": lambda w: w.sum() - 1},
options={"ftol": 1e-12, "maxiter": 1000},
)
if not result.success:
raise RuntimeError(result.message)
return result.x
individual_vols = np.sqrt(np.diag(expected_cov))
common_sharpe = 0.4
diversification_weights = maximize_premium_ratio(common_sharpe * individual_vols, expected_cov)
diversification_vol = np.sqrt(diversification_weights @ expected_cov @ diversification_weights)
diversification_ratio = (diversification_weights @ individual_vols) / diversification_vol
print("Maximum-DR weights (%):", np.round(100 * diversification_weights, 4))
print(f"Diversification ratio: {diversification_ratio:.6f}")
print(f"Portfolio Sharpe: {common_sharpe * diversification_ratio:.6f}")
ได้ maximum-DR weights ประมาณ 46.6541%, 29.2491%, 24.0968% ค่า DR เท่ากับ 1.589394 และ Sharpe ของพอร์ตประมาณ น้ำหนักนี้ต่างจากทั้ง GMV และ equal-weight เพราะสมมติฐาน expected returns ต่างกัน
result.success ตรวจว่า solver รายงานการจบสำเร็จ ถ้าไม่สำเร็จฟังก์ชันหยุดด้วย error เพื่อไม่ส่งน้ำหนักที่ยังไม่ผ่านการแก้โจทย์กลับไปใช้ การสำเร็จของ solver ไม่ได้ตรวจว่าทุกสินทรัพย์มี Sharpe 0.4 จริง รายละเอียด argument อยู่ในเอกสาร SciPy
สมมติฐาน equal Sharpe ให้ผลตอบแทนคาดหวังชดเชย total volatility ทั้งหมด หาก volatility ส่วนหนึ่งเป็นความเสี่ยงเฉพาะตัวที่กระจายออกได้ การเพิ่มส่วนนี้อาจไม่ทำให้ผลตอบแทนคาดหวังสูงขึ้นตามสัดส่วน ทฤษฎี pricing อย่าง CAPM จึงเสนอให้ผูกผลตอบแทนกับ systematic exposure ภายใต้สมมติฐานของทฤษฎีแทน การเลือก equal Sharpe ยังเป็นทางเลือกเชิงแบบจำลอง และเงื่อนไข จำเป็นต่อการรักษาทิศทางการ maximize เช่นเดียวกับกรณี equal means
ใช้ CAPM เพื่อกำหนด Expected returns จาก Beta
ทบทวน CAPM และ beta: beta วัดความไวต่อ market excess return ส่วน CAPM เพิ่มข้อกำหนดด้าน pricing ว่า
การประมาณ beta จาก regression ยังไม่ทำให้สมการ pricing เป็นจริงโดยอัตโนมัติ หากใช้ CAPM forecast เรากำลังตั้งสมมติฐานว่า expected alpha เป็นศูนย์ และ market premium ที่ใส่เหมาะกับช่วงอนาคตที่ต้องการประเมิน
ตัวอย่างใหม่เป็นรายปี ให้ beta ของ A/B/C เท่ากับ 0.7, 1.0, 1.3 ตั้ง และ market premium 5% จะได้ expected returns เท่ากับ , 7% และ 8.5% ค่า มีหน่วยผลตอบแทนต่อปี ส่วน beta ไม่มีหน่วยเมื่อทั้ง market และสินทรัพย์ใช้ return หน่วยเดียวกัน
เราสร้าง covariance ให้สอดคล้องกับ factor model นี้ โดยใช้ market SD 18% และ residual SD ของสินทรัพย์ 12%, 10%, 15% สมมติ residual ไม่สัมพันธ์กับตลาดและไม่สัมพันธ์ข้ามสินทรัพย์ จึงได้ ตามบท covariance จาก factor
market_betas = np.array([0.7, 1.0, 1.3])
market_vol, market_premium, capm_rf = 0.18, 0.05, 0.02
idiosyncratic_vols = np.array([0.12, 0.10, 0.15])
capm_cov = market_vol ** 2 * np.outer(market_betas, market_betas) + np.diag(idiosyncratic_vols ** 2)
capm_expected_returns = capm_rf + market_betas * market_premium
capm_weights = maximize_premium_ratio(market_betas * market_premium, capm_cov)
capm_weights_double = maximize_premium_ratio(market_betas * (2 * market_premium), capm_cov)
capm_portfolio_beta = capm_weights @ market_betas
capm_portfolio_vol = np.sqrt(capm_weights @ capm_cov @ capm_weights)
print("CAPM annual expected returns (%):", np.round(100 * capm_expected_returns, 4))
print("CAPM weights (%):", np.round(100 * capm_weights, 4))
print("Positive premium rescaling preserves weights:", np.allclose(capm_weights, capm_weights_double, atol=1e-5))
print(f"Portfolio beta: {capm_portfolio_beta:.6f}; annual SD: {capm_portfolio_vol:.4%}")
print(f"CAPM portfolio Sharpe: {market_premium * capm_portfolio_beta / capm_portfolio_vol:.6f}")
ได้พอร์ต long-only ประมาณ 23.5532%, 48.4522%, 27.9946% มี portfolio beta 1.013324, annual volatility 19.5394% และ expected Sharpe 0.259302 ภายใต้ข้อมูลเข้าชุดนี้
เพราะ เราจึงเขียน
เมื่อ ค่าเดียวกันทุกสินทรัพย์ การเปลี่ยนจาก premium 5% เป็น 10% ไม่เปลี่ยนน้ำหนัก MSR ในตัวอย่าง จึงได้ True เมื่อตรวจเทียบด้วย tolerance ของการคำนวณ แต่ expected return และ Sharpe ของพอร์ตยังเพิ่มตาม premium หาก premium ศูนย์จะไม่เหลือการจัดอันดับด้วย Sharpe แบบนี้ และหาก premium ติดลบการตัดตัวคูณออกต้องกลับทิศการหาค่าสูงสุด
ภายใต้ CAPM อัตราส่วน เรียกว่า Treynor ratio และเท่ากับ เมื่อ ไม่ควรหารด้วย beta ศูนย์ และ Treynor ที่มี beta ติดลบต้องอ่านพร้อมทิศทาง exposure ไม่ใช้จัดอันดับแบบ reward-to-total-risk โดยตรง
พอร์ตที่หาในโค้ดเลือกได้เฉพาะสินทรัพย์สมมติสามตัว ความเสี่ยง residual ยังเหลืออยู่แม้ใช้ CAPM forecast การตั้ง expected alpha เป็นศูนย์ไม่ได้ทำให้ residual variance เป็นศูนย์
Regression ที่ดีในอดีตยังต้องมีสมมติฐานเพื่อพยากรณ์
จากบทหลายปัจจัย เขียน model ของ excess return เป็น
เป็น intercept, เป็น loading, เป็น factor return และ เป็น residual ถ้า loading คงที่และ residual มี expected value ศูนย์ แบบจำลองให้
ในตัวอย่างต่อไป factor เป็นผลตอบแทนที่ซื้อขายได้: Market เป็น market excess return ส่วน Value เป็นผลต่างผลตอบแทนของสองขา จึงไม่มีการหัก risk-free rate ซ้ำจาก Value คำว่า Value เป็นชื่อปัจจัยสมมติ ไม่ใช่ชุดข้อมูล Fama–French จริง สำหรับปัจจัยมหภาคที่มีหน่วยอย่างการเติบโตของผลผลิต ราคาของความเสี่ยง ไม่ใช่ค่าเฉลี่ยดิบของตัวแปรมหภาคโดยอัตโนมัติ ต้องระบุ pricing model เพิ่ม
เปลี่ยนกลับเป็นข้อมูลรายเดือน สร้างสอง factor แปดเดือนและสินทรัพย์สามตัวโดยกำหนด intercept/loadings ล่วงหน้า เราใช้แพตเทิร์น u, v, q ที่มีค่าเฉลี่ยศูนย์และตั้งฉากกันใน sample เพื่อให้ตรวจผล regression ได้ ไม่ได้อ้างว่าลำดับสมมติเหล่านี้เป็น IID หรือทำซ้ำตลาด
factor_design มีหนึ่งคอลัมน์ของเลข 1 สำหรับ intercept แล้วตามด้วย factor สองคอลัมน์ จึงมีขนาด 8×3 lstsq หาค่าสัมประสิทธิ์ที่ทำให้ผลรวม residual กำลังสองต่ำสุดพร้อมกันสามสินทรัพย์ เมทริกซ์สัมประสิทธิ์มีขนาด 3×3: แถวแรกเป็น intercept ส่วนสองแถวถัดไปเป็น loadings การ transpose สองแถวหลังทำให้ fitted_loadings มีหนึ่งแถวต่อสินทรัพย์และหนึ่งคอลัมน์ต่อ factor
u = np.array([-1, -1, -1, -1, 1, 1, 1, 1])
v = np.array([-1, -1, 1, 1, -1, -1, 1, 1])
q = np.array([-1, 1, -1, 1, -1, 1, -1, 1])
factor_returns = pd.DataFrame({"Market": 0.005 + 0.03 * u, "Value": 0.002 + 0.015 * v},
index=pd.period_range("2024-01", periods=8, freq="M"))
planted_alpha = np.array([0.001, -0.0005, 0.0008])
planted_loadings = np.array([[0.7, 0.3], [1.0, -0.2], [1.3, 0.5]])
factor_rf = 0.001
asset_excess = planted_alpha + factor_returns.to_numpy() @ planted_loadings.T + q[:, None] * np.array([0.006, 0.008, 0.004])
asset_returns = pd.DataFrame(factor_rf + asset_excess, index=factor_returns.index, columns=["A", "B", "C"])
factor_design = np.column_stack([np.ones(len(factor_returns)), factor_returns])
fitted_coefficients = np.linalg.lstsq(factor_design, asset_excess, rcond=None)[0]
fitted_alpha, fitted_loadings = fitted_coefficients[0], fitted_coefficients[1:].T
reconstructed_means = factor_rf + fitted_alpha + fitted_loadings @ factor_returns.mean().to_numpy()
print("Fitted monthly alpha (%):", np.round(100 * fitted_alpha, 4))
print("Fitted factor loadings:\n", np.round(fitted_loadings, 4))
print("Monthly asset sample means (%):", np.round(100 * asset_returns.mean().to_numpy(), 4))
print("Full OLS mean identity:", np.allclose(reconstructed_means, asset_returns.mean().to_numpy()))
ได้ fitted alpha เท่ากับ 0.10%, −0.05%, 0.08% ต่อเดือน และ loading ตรงกับค่าที่ใช้สร้างข้อมูล ค่าเฉลี่ยผลตอบแทนรวมของ A/B/C เท่ากับ 0.61%, 0.51%, 0.93% ต่อเดือน เราทำให้ residual ของสินทรัพย์ใช้ q ร่วมกัน จึงไม่ตั้งสมมติฐานว่า residual ทั้งสามเป็นอิสระจากกัน
ผล Full OLS mean identity: True มาจากคุณสมบัติของ OLS ที่มี intercept: residual ในช่วง fit มีผลรวมศูนย์ ดังนั้น
ถ้าใช้ fitted intercept และ sample factor means จากช่วงเดียวกันทั้งหมด การคำนวณผ่าน factor model จะคืนค่าเฉลี่ยสินทรัพย์เดิม จึงยังไม่มีการหดค่าเฉลี่ยหรือเพิ่มข้อมูลอนาคต การตั้ง การหด alpha หรือการใช้ premium จากแหล่งอื่นล้วนเป็นสมมติฐานเพิ่มเติมที่ต้องระบุให้ชัดเจน
การแยก expected returns เป็นผลจาก factor และ expected residual อธิบายไว้ใน Sharpe: Factor-based Expected Returns หากต้องประมาณ intercept อนาคตแยกทุกสินทรัพย์ เราก็ยังเหลือปัญหาการประมาณ mean รายสินทรัพย์ แม้จะเขียนสมการในรูป factor แล้วก็ตาม
Factor premium หลายตัวทำให้ต้องเลือกความเชื่อเพิ่มเติม
เมื่อมีเพียง market premium บวกตัวเดียว เราตัดตัวคูณร่วมออกจากโจทย์ MSR ได้ แต่ในหลาย factor อัตราส่วนระหว่าง เปลี่ยน expected returns ข้ามสินทรัพย์และอาจเปลี่ยนพอร์ตด้วย การรู้ loading จึงยังไม่พอ
ใช้ loading จากตัวอย่างเดิม กำหนด risk-free rate สำหรับเดือนอนาคตเป็น 0.15% ต่อเดือน และกำหนด forecast alpha เป็นศูนย์ เราจะเปรียบเทียบ premium สี่ชุดซึ่งล้วนเป็นสมมติฐานในตัวอย่าง:
| ชื่อในโค้ด | Market premium ต่อเดือน | Value premium ต่อเดือน | ที่มา |
|---|---|---|---|
historical_premia |
0.50% | 0.20% | ค่าเฉลี่ย factor แปดเดือนที่สร้างไว้ |
cautious |
0.30% | 0.00% | ค่าที่เลือกสมมติก่อนดูช่วงประเมิน |
negative_value |
0.30% | −0.20% | ทดลองผลของ Value premium ติดลบ |
equal_factor_Sharpe |
คำนวณจาก | คำนวณจาก | สมมติ Sharpe รายเดือนของ factor เท่ากับ 0.2 |
ชื่อ cautious เป็นป้ายของสถานการณ์ ไม่ได้รับรองว่าเป็น forecast ที่ปลอดภัยกว่า ตัวอย่างนี้ใช้ future ใหม่ ไม่ได้เอา risk-free rate ในอดีตมาแทนอนาคตโดยอัตโนมัติ ส่วน zero expected alpha หมายถึงเราไม่พยากรณ์ผลตอบแทนส่วนเพิ่มที่แยกจาก factor ยังมี residual risk ตามเดิม
future_rf = 0.0015
factor_premium_scenarios = pd.DataFrame({
"historical_premia": factor_returns.mean(),
"cautious": [0.003, 0.0],
"negative_value": [0.003, -0.002],
"equal_factor_Sharpe": 0.2 * factor_returns.std(ddof=1),
}, index=factor_returns.columns)
forecast_alpha = np.zeros(3)
factor_forecasts = pd.DataFrame(
future_rf + forecast_alpha[:, None] + fitted_loadings @ factor_premium_scenarios.to_numpy(),
index=asset_returns.columns, columns=factor_premium_scenarios.columns,
)
print("Factor premium assumptions (% per month):\n", (100 * factor_premium_scenarios).round(4))
print("Asset expected returns (% per month):\n", (100 * factor_forecasts).round(4))
สถานการณ์ historical_premia ให้ expected returns ของ A/B/C เท่ากับ 0.56%, 0.61%, 0.90% ต่อเดือน เช่น A เท่ากับ ตัวเลขต่างจาก sample means ก่อนหน้าเพราะเปลี่ยน และตั้ง forecast alpha ศูนย์
สถานการณ์ cautious ให้ 0.36%, 0.45%, 0.54% ต่อเดือน เมื่อเปลี่ยน Value premium เป็น −0.20% ผลของ B เพิ่มเป็น 0.49% เพราะ loading ของ B ต่อ Value เท่ากับ −0.2 ผลคูณ loading ลบกับ premium ลบจึงบวก 0.04 จุดเปอร์เซ็นต์ ส่วนสินทรัพย์ที่ loading บวกถูกลด expected return
กรณี equal factor Sharpe ใช้ sample SD รายเดือนประมาณ 3.2071% และ 1.6036% จึงตั้ง premia ประมาณ 0.6414% และ 0.3207% ต่อเดือน สมมติฐานว่า factor มี reward ต่อความเสี่ยงเท่ากันนี้ต่างจากสมมติฐาน equal Sharpe ของสินทรัพย์ทุกตัว เพราะสินทรัพย์มี loading และ residual risk ต่างกัน
การใช้ premium ย้อนหลังช่วงยาวช่วยเพิ่มข้อมูลภายใต้ความคงที่ แต่ไม่กำจัดความไม่แน่นอนของค่าเฉลี่ย factor ผลตอบแทนหลังค้นพบปัจจัยอาจต่างจากช่วงที่ใช้ค้นพบ วิธีนิยามปัจจัย ต้นทุน และ universe อาจเปลี่ยนได้ จึงควรดูหลายสมมติฐานและช่วงข้อมูลที่กำหนดไว้ล่วงหน้า แทนการเลือกย้อนหลังเฉพาะช่วงที่ให้พอร์ตดูดีที่สุด
ตรึง Forecast ก่อนเปิดข้อมูลช่วงประเมิน
สมมติเราตัดสินใจเมื่อสิ้นเดือนสิงหาคม 2024 ใช้ข้อมูลมกราคมถึงสิงหาคมที่สร้างไว้ และกำหนด forecast คงที่สำหรับการเปรียบเทียบสามเดือนถัดไปไว้สองแบบ แบบแรกใช้ sample mean ของสินทรัพย์ แบบที่สองใช้ factor forecast สถานการณ์ cautious ทุกสมมติฐานต้องถูกกำหนดก่อนเปิดผลตอบแทนกันยายนถึงพฤศจิกายน
เราจะคำนวณ root mean squared error หรือ RMSE โดยหาผลต่างระหว่างผลตอบแทนแต่ละเดือนกับ forecast ยกกำลังสอง เฉลี่ยทุกเดือนและทุกสินทรัพย์ แล้วถอดราก ให้ เป็นจำนวนเดือนประเมินและ เป็นจำนวนสินทรัพย์:
การยกกำลังสองทำให้ข้อผิดพลาดบวกและลบไม่หักล้างกัน การถอดรากทำให้หน่วยกลับมาเหมือนผลตอบแทน RMSE จึงรวมทั้งข้อผิดพลาดของ mean forecast และความผันผวนของผลตอบแทนที่เกิดจริง ไม่ใช่ค่าที่สังเกต estimation error ของ จริงได้โดยตรง
frozen_forecasts = pd.DataFrame({
"sample_mean": asset_returns.mean(),
"factor_prior": factor_forecasts["cautious"],
})
heldout_returns = pd.DataFrame([[0.006, -0.002, 0.012],
[-0.010, 0.005, -0.015],
[0.020, 0.008, 0.010]],
index=pd.period_range("2024-09", periods=3, freq="M"),
columns=asset_returns.columns)
assert factor_returns.index.max() < heldout_returns.index.min()
forecast_rmse = pd.Series({
name: np.sqrt(np.mean((heldout_returns.to_numpy() - frozen_forecasts[name].to_numpy()) ** 2))
for name in frozen_forecasts.columns
})
print("Frozen forecasts (% per month):\n", (100 * frozen_forecasts).round(4))
print("Held-out realized means (% per month):", np.round(100 * heldout_returns.mean().to_numpy(), 4))
print("RMSE across months and assets (percentage points):\n", (100 * forecast_rmse).round(4))
assert ตรวจว่าวันท้ายของช่วงฝึกมาก่อนวันแรกของช่วงประเมิน frozen_forecasts เก็บ forecast ก่อนสร้างตัวอย่างช่วงประเมิน ส่วนลูป for name in frozen_forecasts.columns คำนวณ RMSE แยกแต่ละวิธีโดยใช้เดือนและสินทรัพย์ชุดเดียวกัน
ผลรวมทั้งเก้าช่องได้ RMSE ประมาณ 1.1103 จุดเปอร์เซ็นต์สำหรับ sample mean และ 1.0516 จุดเปอร์เซ็นต์สำหรับ factor prior ความต่างในข้อมูลสมมติสามเดือนไม่ได้พิสูจน์ว่า factor prior ดีกว่าโดยทั่วไป หากดูผลนี้ก่อนแล้วกลับไปปรับ premium ให้ RMSE ต่ำลง ข้อมูลกันยายนถึงพฤศจิกายนจะกลายเป็นข้อมูลที่ใช้เลือกแบบจำลอง และต้องกันข้อมูลช่วงใหม่ไว้ประเมินอีกครั้ง
ในการทดสอบพอร์ต ยังต้องตรึง covariance estimator, constraints, วิธีปรับพอร์ต และต้นทุนให้เปรียบเทียบกันได้ การใช้ realized factor returns ของเดือนที่ยังไม่มาถึงไปคำนวณ “forecast” ของเดือนนั้นเป็นการใช้ข้อมูลอนาคต แม้สมการ factor จะคำนวณถูกทุกพจน์ก็ตาม
บทถัดไป Implied Returns และ Views จะเริ่มจากน้ำหนักพอร์ตอ้างอิงแล้วถามกลับว่าค่าคาดหวังแบบใดรองรับน้ำหนักนั้น จากนั้นบท Black–Littermanจะรวม prior กับมุมมองและระดับความไม่แน่นอนของมุมมองอย่างเป็นระบบ
แบบฝึกหัดพร้อมแนวคำตอบ
1. ค่าเฉลี่ยเลขคณิตกับการทบต้น
พอร์ตหนึ่งเริ่มที่ 100 และจบที่ 144 หลังสองปี เส้นทางแรกได้ 20% ทั้งสองปี เส้นทางที่สองได้ −20% แล้ว 80% ค่าเฉลี่ยเลขคณิตและ CAGR ของสองเส้นทางเท่ากันหรือไม่?
เฉลย: ผลคูณของทั้งสองเส้นทางเป็น 1.44 จึงมี CAGR เท่ากัน แต่ค่าเฉลี่ยเลขคณิตเป็น 20% กับ ตามลำดับ การรู้มูลค่าต้นและปลายอย่างเดียวจึงไม่กำหนดค่าเฉลี่ยเลขคณิตของผลตอบแทนรายช่วง
2. SD สูงไม่ได้แปลว่า SE เท่ากับ SD
สมมติผลตอบแทน IID Normal มี SD ที่รู้แน่นอน 4% ต่อเดือน เก็บ 100 เดือน และได้ sample mean 0.5% ต่อเดือน SE และช่วง mean แบบ Normal 95% โดยใช้ค่าขอบ 1.96 เป็นเท่าไร?
เฉลย: SE เท่ากับ จุดเปอร์เซ็นต์ ช่วง mean เป็น หรือ −0.284% ถึง 1.284% ต่อเดือน โจทย์ให้ population SD จึงใช้ Normal critical value หากประมาณ SD จากข้อมูลจะใช้ t critical value ตามเงื่อนไขของสูตรก่อนหน้า ช่วงนี้ยังไม่ใช่ prediction interval ของเดือนหน้า
3. ต้องเพิ่มประวัติเท่าไรจึงลด SE ครึ่งหนึ่ง?
ใน additive IID model annual SD 20% ประวัติสี่ปีให้ SE ของ annual mean เท่าไร และต้องมีประวัติกี่ปีจึงลด SE ครึ่งหนึ่ง?
เฉลย: ได้ จุดเปอร์เซ็นต์ ต้องเพิ่มเป็น 16 ปีจึงได้ จุดเปอร์เซ็นต์ การเปลี่ยนสี่ปีเดิมจากข้อมูลรายเดือนเป็นรายวันไม่ให้ผลเท่ากับเพิ่มเป็น 16 ปี
4. Bayesian update ต้องรอข้อมูลมากก่อนหรือไม่?
เริ่มจาก prior แล้วเห็นหัวหก ก้อยสี่ หากครั้งถัดไปออกหัว ค่าเฉลี่ย posterior เปลี่ยนอย่างไร?
เฉลย: หลังสิบครั้ง posterior เป็น มี mean เมื่อเพิ่มหัวอีกหนึ่งเป็น มี mean ข้อมูลใหม่อัปเดต posterior ได้ทันทีภายใต้แบบจำลองนี้
5. Shrinkage target ไม่ใช่น้ำหนักพอร์ต
ค่าประมาณ expected returns รายปีเป็น 10% และ 2% เลือก target ร่วม 6% และ ได้ expected returns ใหม่เท่าไร และบอกได้หรือยังว่าต้องถือ 50/50?
เฉลย: ได้ และ ยังสรุปน้ำหนักไม่ได้ ต้องรู้ covariance, objective และ constraints ด้วย แม้หดจน expected returns เท่ากัน GMV ก็ไม่จำเป็นต้องเป็น equal-weight
6. เมื่อ common excess return ติดลบ
ทุกสินทรัพย์มี expected return 1% ต่อปี แต่ พอร์ตหนึ่งมี volatility 8% อีกพอร์ต 20% พอร์ตใดมี Sharpe มากกว่า?
เฉลย: Sharpe เป็น และ พอร์ต volatility สูงกว่ามี Sharpe มากกว่าในโจทย์นี้ จึงใช้ข้อสรุป equal means → MSR คือ GMV โดยไม่ระบุ positive common premium ไม่ได้
7. คูณ Loading กับ Premium โดยดูเครื่องหมายและหน่วย
สินทรัพย์มี loadings ต่อ Market/Value เท่ากับ 1.1/−0.4 คาด premia รายเดือน 0.4%/0.2% และ ต่อเดือน ถ้าตั้ง expected alpha ศูนย์ ได้ expected return เท่าไร?
เฉลย: ต่อเดือน หากเพิ่ม expected alpha 0.05% ต่อเดือนจะเป็น 0.51% การเพิ่ม alpha เป็นสมมติฐาน forecast เพิ่มเติม ไม่ใช่สิ่งที่ต้องใส่ตาม fitted historical alpha เสมอ
8. OLS fit ตรงค่าเฉลี่ยแล้วพิสูจน์อะไรได้?
ใช้ intercept และ factor means จากช่วงฝึกเดียวกันแล้วสร้างค่าเฉลี่ยสินทรัพย์กลับมาได้ตรง จากนั้นลอง premium 50 ชุดและเลือกชุดที่ RMSE ต่ำสุดในสามเดือนประเมิน เรามีหลักฐานพยากรณ์ที่น่าเชื่อถือพอหรือยัง?
เฉลย: การคืน sample mean เป็นเอกลักษณ์ของ OLS ที่มี intercept ส่วนการเลือกจาก 50 ชุดใช้ช่วงประเมินมาช่วยปรับแบบจำลองแล้ว ต้องแยกช่วงเลือกวิธีออกจากช่วงทดสอบถัดไป พร้อมพิจารณาความยาวข้อมูลและความไม่แน่นอนของคะแนน ไม่มีขั้นใดพิสูจน์ว่า expected returns ในอนาคตตรงกับ forecast ที่เลือก
แหล่งเรียนและขอบเขตของบท
อ่าน Transcript เต็มเมื่อ 3 ตุลาคม 2026 ของ Lack of Robustness of Expected Return Estimates, Agnostic Priors on Expected Return Estimates และ Using Factor Models to Estimate Expected Returns ใช้เป็นโครงเรื่อง estimation error, mean shrinkage, agnostic assumptions และ factor premia ตัวอย่าง โค้ด คำอธิบายไทย และแบบฝึกหัดเขียนขึ้นใหม่ ไม่ใช่การทำซ้ำตาราง sector portfolios หรือผลทดสอบของวิดีโอ
สูตร SE และ confidence interval ตรวจจาก NIST: Confidence Limits for the Mean และ What are confidence intervals? การอัปเดต Beta–Bernoulli ตรวจจาก Stanford STATS 200, Lecture 21 สมการ factor expected returns ตรวจจาก Sharpe: Factor-based Expected Returns, Risks and Correlations ส่วน API ที่ใช้ดู SciPy: Student's t และ SciPy: minimize
ในการอ่านประกอบคอร์ส ให้แยกผลตอบแทนสะสม อัตราทบต้น และค่าเฉลี่ยเลขคณิตออกจากกัน การใช้ช่วงข้อมูลต่างกันให้ sample means ต่างกันได้ แต่ค่าเฉลี่ยติดลบไม่ได้เป็นสิ่งที่เป็นไปไม่ได้โดยนิยาม บทนี้จึงตรวจสมมติฐานและความไม่แน่นอนของค่าประมาณก่อนแปลความหมายด้านพอร์ต พร้อมระบุเงื่อนไข premium บวกในเอกลักษณ์ GMV/DR/CAPM และให้ Bayesian update รับข้อมูลทุกครั้งตามแบบจำลองที่เลือก