การสร้างพอร์ต · บทย่อย 3
เลือกพอร์ตเมื่อค่าประมาณคลาดเคลื่อน
หากคาดผลตอบแทนต่างจากเดิมเพียง 0.1 จุดเปอร์เซ็นต์แล้วน้ำหนักเปลี่ยนจากครึ่งต่อครึ่งเป็นถือสินทรัพย์เดียว เราควรเชื่อคำตอบนั้นแค่ไหน?
บท Efficient Frontier ให้เราเลือกน้ำหนักที่ดีที่สุดภายใต้ expected return, covariance และข้อจำกัดที่กำหนด บทนี้เพิ่มสินทรัพย์ปลอดความเสี่ยงเพื่อเลือกส่วนผสม แล้วลองเปลี่ยนข้อมูลเข้าเพื่อดูว่าคำตอบไวต่อค่าประมาณเพียงใด
เนื้อหาเรียบเรียงประกอบหัวข้อ fund separation, maximum Sharpe ratio และข้อจำกัดของ Markowitz analysis พร้อม Lab 110–111 ตัวเลขทั้งหมดเป็นแบบจำลองหรือตัวอย่างสมมติ เราจะใช้พารามิเตอร์สามสินทรัพย์จากบทก่อนในส่วนแรก แล้วระบุชัดเมื่อเปลี่ยนไปใช้ชุดสาธิตความไวหรือข้อมูล train/test
โค้ดเริ่ม Notebook ใหม่ได้ ไม่ต้องมีตัวแปรจากหน้าก่อน ใช้ NumPy, pandas และ SciPy ตัวอย่าง optimization เป็นพอร์ตหนึ่งช่วงเวลา หน่วยต่อปี ไม่มี short ในส่วนสินทรัพย์เสี่ยง ไม่มีภาษีและต้นทุนซื้อขาย ส่วนการกู้ยืมจะกล่าวแยกในหัวข้อการผสมพอร์ต
ดาวน์โหลด Notebook ของบทนี้ เพื่อรันตัวอย่างตามลำดับและเทียบผลลัพธ์ที่บันทึกไว้
เพิ่มสินทรัพย์ที่รู้ผลตอบแทนล่วงหน้าในช่วงที่วัด
ในแบบจำลอง risk-free asset ให้ผลตอบแทนที่ทราบแน่นอนสำหรับช่วงเวลาและหน่วยสกุลเงินที่กำลังพิจารณา สมมติหนึ่งปีได้ คงที่ จึงมี variance เป็นศูนย์และ covariance กับผลตอบแทนสินทรัพย์อื่นเป็นศูนย์ ตัวห้อย ใช้แทน risk-free
คำนี้ต้องอ่านพร้อมช่วงเวลา เช่น ตราสารที่ทราบกระแสเงินสดเมื่อครบกำหนดอาจมีราคาผันผวนถ้าขายก่อนกำหนด และผลตอบแทนที่แน่นอนในหน่วยบาทยังไม่ได้รับรองกำลังซื้อหลังเงินเฟ้อ หรือมูลค่าเมื่อแปลงเป็นอีกสกุลเงิน บทนี้ใช้สินทรัพย์ปลอดความเสี่ยงตามนิยามของแบบจำลองเพื่อฝึกสูตร
ให้พอร์ตสินทรัพย์เสี่ยงชุดหนึ่งมี expected return และ SD ถ้าลงทุนสัดส่วน ในพอร์ตนั้น และเหลือ ใน risk-free asset จะได้
ใช้แทนพอร์ตรวม ส่วนค่า absolute value ทำให้ SD ไม่ติดลบ ช่วงแรกเราพิจารณา จึงเป็นการแบ่งเงินที่มีระหว่างเงินปลอดความเสี่ยงกับพอร์ตเสี่ยง ค่า ทำให้ถือพอร์ตเสี่ยงครึ่งหนึ่งและ risk-free อีกครึ่ง
ตัวอย่างพอร์ตเสี่ยงที่ mean 10% และ SD 15% ต่อปี ผสมครึ่งหนึ่งกับ risk-free 2% จะให้ mean และ SD ต่อปี การลดความผันผวนในสูตรนี้มาพร้อมกับลด expected return เข้าหา 2%
Sharpe ratio คือความชันจากจุด risk-free
เมื่อกำหนดสินทรัพย์ปลอดความเสี่ยงแล้ว สิ่งที่ผู้ลงทุนคาดว่าจะได้รับเพิ่มจากรับความเสี่ยงคือ expected excess return เราหารส่วนเพิ่มนี้ด้วย SD เพื่อได้ Sharpe ratio ของแบบจำลองหนึ่งช่วงเวลา:
ในกราฟที่แกนนอนเป็น SD และแกนตั้งเป็น expected return จุด risk-free อยู่ที่ ความชันของเส้นจากจุดนั้นไปยังพอร์ตเสี่ยงคือ Sharpe ratio พอดี พอร์ตที่ได้ผลตอบแทนสูงกว่าอาจไม่ได้มี Sharpe สูงกว่า หากต้องเพิ่ม SD มากกว่าส่วนผลตอบแทนที่เพิ่มขึ้น William F. Sharpe: The Sharpe Ratio
ถ้าจะประมาณ Sharpe จากข้อมูลรายเดือน ให้คำนวณ excess return รายเดือนแล้วใช้ค่าเฉลี่ยเลขคณิตหารด้วย SD ของ excess return ในความถี่เดียวกัน เมื่อ risk-free คงที่ SD ของ excess return เท่ากับ SD ของผลตอบแทนพอร์ต การคูณ Sharpe รายเดือนด้วย อาศัยเงื่อนไขการแปลงเวลาที่เหมาะสม โดยเฉพาะความสัมพันธ์ข้ามเวลา ส่วน CAGR หาร SD เป็นตัววัดอีก convention หนึ่งและไม่ตรงกับนิยาม arithmetic Sharpe นี้เสมอไป
ใน Lab บางช่วงใช้ annualized compounded return เป็นตัวเลขผลตอบแทนบนกราฟ เราใช้ expected arithmetic return ต่อปี ที่สมมติขึ้นในหน้านี้ เพื่อให้ , risk-free และ covariance อยู่ในช่วงเวลาเดียวกันอย่างชัดเจน ควรอ่านฟังก์ชันที่คำนวณตัวเศษก่อนเปรียบเทียบ Sharpe จากคนละโปรแกรม
หา Maximum Sharpe Ratio portfolio จากแบบจำลองเดิม
ใช้ A/B/C mean 6%, 10%, 14% ต่อปี และ covariance จากบทก่อน น้ำหนักรวมหนึ่งและอยู่ระหว่างศูนย์กับหนึ่ง ในขั้นนี้ทุกน้ำหนักเป็นน้ำหนักภายในพอร์ตสินทรัพย์เสี่ยง ยังไม่รวมสัดส่วน risk-free ที่จะผสมภายหลัง
import numpy as np
import pandas as pd
from scipy.optimize import minimize
asset_names = ["A", "B", "C"]
annual_mu = np.array([0.06, 0.10, 0.14])
annual_cov = np.array([
[0.0100, 0.0030, 0.0020],
[0.0030, 0.0225, 0.0090],
[0.0020, 0.0090, 0.0400]
])
annual_rf = 0.02
SciPy มีคำสั่งหาค่าต่ำสุด เราจึงส่งค่า Sharpe ที่กลับเครื่องหมายให้มันลด ยิ่ง negative Sharpe ต่ำเท่าไร Sharpe เดิมยิ่งสูง สูตรเป้าหมายคือ
เริ่มจากฟังก์ชันที่รับ objective และจำนวนสินทรัพย์ แล้วคืนค่าน้ำหนักที่ผ่านการตรวจ ใช้ lambda สำหรับฟังก์ชันสั้นของข้อจำกัดน้ำหนักรวมหนึ่ง หากยังไม่คุ้นกับ objective, bounds หรือ SLSQP ให้กลับไปอ่านการตั้งโจทย์ optimization
def solve_weights(objective, n_assets):
result = minimize(
objective, np.full(n_assets, 1 / n_assets),
method="SLSQP", bounds=[(0, 1)] * n_assets,
constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1}],
options={"ftol": 1e-12, "maxiter": 1000}
)
if not result.success:
raise RuntimeError(result.message)
w = result.x
assert np.isclose(w.sum(), 1, atol=1e-8)
assert (w >= -1e-8).all() and (w <= 1 + 1e-8).all()
return w
ต่อไปสร้าง find_msr โดย msr ย่อจาก maximum Sharpe ratio ฟังก์ชันข้างในคำนวณ Sharpe ติดลบสำหรับน้ำหนักแต่ละชุด แล้วให้ solve_weights ช่วยค้นหา เงื่อนไข variance <= 0 หยุดกรณีที่ตัวหารใช้ไม่ได้; covariance ชุดสมมติของเรามี variance เป็นบวกสำหรับพอร์ตที่ใช้เงินครบ
def find_msr(mu, cov, rf):
def negative_sharpe(w):
variance = float(w @ cov @ w)
if variance <= 0:
raise ValueError("Sharpe requires positive portfolio variance")
return -(w @ mu - rf) / np.sqrt(variance)
return solve_weights(negative_sharpe, len(mu))
msr_weights = find_msr(annual_mu, annual_cov, annual_rf)
msr_mean = float(msr_weights @ annual_mu)
msr_sd = np.sqrt(msr_weights @ annual_cov @ msr_weights)
msr_sharpe = (msr_mean - annual_rf) / msr_sd
print(pd.Series(msr_weights, index=asset_names).round(6))
print(f"Mean: {msr_mean:.4%}; SD: {msr_sd:.4%}; Sharpe: {msr_sharpe:.6f}")
ได้ A ประมาณ 38.3984%, B 29.9795%, C 31.6222%, expected return 9.7290%, SD 10.1879% ต่อปี และ Sharpe ประมาณ 0.758640 น้ำหนักไม่ได้เลือก C ทั้งหมดแม้ C มี expected return สูงสุด เพราะ objective คิดทั้งส่วนเพิ่มเหนือ risk-free และความเสี่ยงของผลรวม
ชื่อ Maximum Sharpe หมายถึงผลที่สูงที่สุดภายใต้แบบจำลองและชุดน้ำหนักที่เราค้นหา ผลลัพธ์เชิงตัวเลขยังต้องตรวจความไวและการเริ่มค้นหาจากจุดอื่นเมื่อใช้โจทย์ที่ซับซ้อนขึ้น เมื่อเปลี่ยน forecast น้ำหนัก MSR ก็อาจเปลี่ยนตาม
ผสมพอร์ตเสี่ยงกับ risk-free และแยก CAL จาก CML
ใช้สัดส่วน risky_share แทน ในสูตรต้นบท abs(...) คืนค่าสัมบูรณ์เพื่อคำนวณ SD และ .append(...) เก็บผลแต่ละส่วนผสมไว้เป็นแถวของตาราง
mix_rows = []
for risky_share in [0.0, 0.5, 1.0, 1.2]:
mix_rows.append({
"Risky share": risky_share,
"Risk-free share": 1 - risky_share,
"Expected return": annual_rf + risky_share * (msr_mean - annual_rf),
"SD": abs(risky_share) * msr_sd
})
cal_table = pd.DataFrame(mix_rows)
print((cal_table * 100).round(4))
| ลงทุนใน MSR | ลงทุนใน risk-free | Expected return ต่อปี | SD ต่อปี |
|---|---|---|---|
| 0% | 100% | 2.0000% | 0.0000% |
| 50% | 50% | 5.8645% | 5.0940% |
| 100% | 0% | 9.7290% | 10.1879% |
| 120% | −20% | 11.2747% | 12.2255% |
แถวสุดท้ายหมายถึงกู้เงิน 20% ของเงินทุนที่อัตรา 2% แล้วนำเงินรวม 120% ไปลงทุนใน MSR การแสดงแถวนี้ต้องสมมติว่ากู้และให้กู้ได้ที่ เดียวกัน รวมถึงไม่มีต้นทุนหรือวงเงินกู้ที่ขัดกับน้ำหนัก หากกู้แพงกว่าอัตราที่รับจากเงินปลอดความเสี่ยง เส้นช่วงลงทุนเกิน 100% จะมีความชันต่างไป และถ้าห้ามกู้ก็ใช้ได้เฉพาะช่วง 0 ถึง 100%
เส้นของส่วนผสม risk-free กับพอร์ตเสี่ยงหนึ่งชุดเรียกว่า Capital Allocation Line หรือ CAL หากพอร์ตเสี่ยงเป็นจุดที่ให้ Sharpe สูงสุด เส้นนี้จะมีความชันสูงที่สุดในกรอบที่กำหนดและสัมผัสขอบของพอร์ตเสี่ยงที่เกี่ยวข้อง
Capital Market Line หรือ CML ใช้ในกรอบดุลยภาพเมื่อพอร์ตเสี่ยงดังกล่าวคือ market portfolio ภายใต้สมมติฐานของแบบจำลอง การคำนวณ MSR จาก A/B/C สามตัวที่เราเลือกและ forecast ของเราเอง จึงไม่ได้ทำให้ CAL นี้เป็น CML ของตลาดโดยอัตโนมัติ CFA Institute: Portfolio Risk and Return, Part II
แนวคิด fund separation แยกสองการตัดสินใจ: เลือกองค์ประกอบพอร์ตเสี่ยง แล้วเลือกสัดส่วนระหว่างพอร์ตเสี่ยงกับ risk-free ตามความเสี่ยงที่ยอมรับ ในกรอบ mean–variance ผู้ลงทุนที่ใช้พารามิเตอร์และชุดสินทรัพย์เดียวกันสามารถใช้พอร์ต MSR ร่วมกันได้ ภายใต้สมมติฐานว่ากู้และให้กู้ที่ เดียวกัน และไม่มีเพดานกู้ที่ขัดกับส่วนผสมที่ต้องการ หาก forecast, ภาษี, ข้อจำกัด short หรืออัตรากู้ยืมต่างกัน ต้องพิจารณาข้อสรุปนี้ใหม่
ถ้าห้ามกู้ แม้ทุกคนเผชิญข้อจำกัดเดียวกัน การผสม MSR กับเงินปลอดความเสี่ยงในตัวอย่างนี้ก็ให้ expected return ได้สูงสุดเพียงประมาณ 9.729% ผู้ที่ต้องการ 12% จึงต้องเลือกพอร์ตเสี่ยงอื่นที่ทำเป้าหมายได้ เช่น B/C อย่างละครึ่ง แล้วค้นหาพอร์ต variance ต่ำสุดสำหรับเป้านั้น การเพิ่มระดับความเสี่ยงจึงไม่ได้ทำได้ด้วยการเปลี่ยนสัดส่วน MSR กับเงินสดอย่างเดียวเสมอไป
แม้ MSR จะ efficient ตาม mean–variance ภายในสามสินทรัพย์นี้ ก็ยังอาจมีความเสี่ยงเฉพาะรายชื่อหรือปัจจัยที่ไม่ได้กระจายออก ไม่ควรเรียกมันว่าพอร์ตที่กำจัด idiosyncratic risk ทั้งหมดเพียงเพราะหา tangent point ได้
GMV และ Equal Weight พึ่งค่าประมาณต่างกันอย่างไร
MSR ใช้ทั้ง expected return และ covariance รวมถึง risk-free rate หากไม่มั่นใจใน expected return เราอาจเลือกพอร์ตที่ลด variance โดยไม่ใส่เป้าผลตอบแทน นั่นคือ GMV จากบทก่อน สูตร objective ใช้ covariance เพียงอย่างเดียว จึงไม่ต้องเดา mean เพื่อหาน้ำหนัก แต่ยังต้องประมาณ covariance ให้เหมาะสม
อีกทางเลือกคือ Equal Weight หรือ EW ซึ่งแบ่งเงินเท่ากันทุกสินทรัพย์ สำหรับสามตัวใช้น้ำหนัก ต่อชื่อ EW ไม่ต้องประมาณ mean หรือ covariance เพื่อคำนวณน้ำหนักเริ่มต้น แต่ยังต้องตัดสินใจเลือกรายการสินทรัพย์ จังหวะปรับพอร์ต และต้นทุน การแบ่งเงินเท่ากันไม่ได้หมายความว่าแต่ละชื่อให้ความเสี่ยงเท่ากัน
def find_gmv(cov):
return solve_weights(lambda w: float(w @ cov @ w), len(cov))
gmv_weights = find_gmv(annual_cov)
ew_weights = np.full(3, 1 / 3)
comparison_rows = []
for name, w in [("MSR", msr_weights), ("GMV", gmv_weights), ("EW", ew_weights)]:
mean = float(w @ annual_mu)
sd = np.sqrt(w @ annual_cov @ w)
comparison_rows.append({"Portfolio": name, "Mean": mean, "SD": sd, "Sharpe": (mean - annual_rf) / sd})
comparison = pd.DataFrame(comparison_rows).set_index("Portfolio")
print(comparison.round(6))
ใน for name, w in ... แต่ละสมาชิกของรายการเป็นคู่ชื่อกับน้ำหนัก เช่น ("EW", ew_weights) Python จะแยกค่าสองตัวให้ใช้ในรอบนั้น ส่วน .set_index("Portfolio") ย้ายชื่อพอร์ตจากคอลัมน์มาเป็นป้ายแถว
| พอร์ต | Expected return ต่อปี | SD ต่อปี | Sharpe |
|---|---|---|---|
| MSR | 9.7290% | 10.1879% | 0.758640 |
| GMV | 7.7126% | 8.7587% | 0.652218 |
| EW | 10.0000% | 10.5672% | 0.757056 |
EW มี expected return สูงกว่า MSR เล็กน้อย แต่ Sharpe ต่ำกว่า ส่วน GMV มี SD ต่ำสุดตาม objective ที่ตั้งไว้ เราไม่ได้ใช้ตารางนี้พิสูจน์ว่าแนวทางใดชนะในตลาด เพราะตัวเลขทุกค่าคำนวณจากพารามิเตอร์เดียวกับที่ใช้เลือกน้ำหนัก และยังไม่มีผลตอบแทนที่เกิดขึ้นหลังเลือกพอร์ต
ชื่อ GMV บอกเป้าหมาย variance ต่ำสุดภายใต้ข้อจำกัดของเรา ไม่ได้รับรองผลตอบแทนบวกหรือไม่มี drawdown ส่วน EW เป็น baseline ที่มีวิธีคำนวณน้ำหนักตรงไปตรงมา ไม่ใช่หลักฐานว่าเหนือกว่าพอร์ตตามมูลค่าตลาดในทุกช่วงเวลา
เปลี่ยน expected return เล็กน้อยแล้วคำนวณใหม่
ปัญหาของ expected return คือเราไม่ทราบล่วงหน้า หากใช้ sample mean มาประมาณ ความคลาดเคลื่อนขึ้นกับทั้งจำนวน observations และความผันผวน ภายใต้การสุ่มอิสระที่มี variance คงที่ standard error ของ sample mean ประมาณ เช่น SD 20% ต่อปีและข้อมูลผลตอบแทนรายปีอิสระ 25 ปี ให้ standard error ของ mean ประมาณ ต่อปี การมีค่าเฉลี่ยย้อนหลังจึงไม่ได้แปลว่า forecast แม่นถึงทศนิยมหลายตำแหน่ง
ในตัวอย่างสามสินทรัพย์ ลองเปลี่ยนเฉพาะ expected return ของ B จาก 10% เป็น 9% หรือ 11% โดย covariance และ risk-free ไม่เปลี่ยน annual_mu.copy() สร้างสำเนาเพื่อไม่เขียนทับพารามิเตอร์หลัก ส่วน [1] เลือกสมาชิกที่สองเพราะ Python เริ่มนับตำแหน่งที่ศูนย์
sensitivity_rows = []
for b_mean in [0.09, 0.10, 0.11]:
changed_mu = annual_mu.copy()
changed_mu[1] = b_mean
changed_weights = find_msr(changed_mu, annual_cov, annual_rf)
sensitivity_rows.append({"B expected return": b_mean, "A": changed_weights[0], "B": changed_weights[1], "C": changed_weights[2]})
sensitivity_table = pd.DataFrame(sensitivity_rows)
print((sensitivity_table * 100).round(4))
| Expected return ของ B | น้ำหนัก A | น้ำหนัก B | น้ำหนัก C |
|---|---|---|---|
| 9% | 41.6400% | 24.0681% | 34.2918% |
| 10% | 38.3984% | 29.9795% | 31.6222% |
| 11% | 35.3816% | 35.4807% | 29.1378% |
เปลี่ยน forecast ของ B หนึ่งจุดเปอร์เซ็นต์ทำให้น้ำหนัก B เปลี่ยนประมาณห้าถึงหกจุดเปอร์เซ็นต์ในตัวอย่างนี้ การเปลี่ยนน้ำหนักเกิดจาก optimizer ทำตาม objective ที่ให้ แต่เราไม่รู้ว่า forecast ที่เปลี่ยนนั้นมีข้อมูลรองรับมากพอหรือเป็นเพียงความคลาดเคลื่อน
สินทรัพย์ที่สัมพันธ์กันสูงอาจทำให้น้ำหนักไปสุดขอบ
แยกมาใช้ตัวอย่างสมมติสองสินทรัพย์ X/Y ทั้งคู่ SD 10% ต่อปี, correlation 0.99 และ risk-free 2% ถ้าคาด mean เท่ากันที่ 8% พอร์ต MSR จะถืออย่างละครึ่ง การแทนกันระหว่างสองตัวเปลี่ยนความเสี่ยงเพียงเล็กน้อย เพราะ correlation สูงมาก
pair_cov = 0.10 ** 2 * np.array([[1.00, 0.99], [0.99, 1.00]])
pair_rows = []
for pair_mu in [[0.08, 0.08], [0.081, 0.079], [0.079, 0.081]]:
pair_weights = find_msr(np.array(pair_mu), pair_cov, 0.02)
pair_rows.append({"Mean X": pair_mu[0], "Mean Y": pair_mu[1], "Weight X": pair_weights[0], "Weight Y": pair_weights[1]})
print((pd.DataFrame(pair_rows) * 100).round(4))
| Mean X | Mean Y | น้ำหนัก X | น้ำหนัก Y |
|---|---|---|---|
| 8.0% | 8.0% | 50% | 50% |
| 8.1% | 7.9% | 100% | 0% |
| 7.9% | 8.1% | 0% | 100% |
เปลี่ยน mean แต่ละตัวเพียง 0.1 จุดเปอร์เซ็นต์ก็ทำให้น้ำหนักไปสุดขอบได้ใน covariance ชุดนี้ เพราะผลตอบแทนคาดหมายที่ต่างกันเล็กน้อยกลายเป็นตัวตัดสินระหว่างสินทรัพย์ที่ความเสี่ยงใกล้กันมาก ไม่ได้หมายความว่าทุก covariance ต้องตอบสนองแรงเท่านี้
การบังคับเพดานน้ำหนัก การจำกัด turnover หรือปรับค่าประมาณเข้าหาค่ากลางอาจลดความไว แต่แลกกับข้อจำกัดหรือสมมติฐานเพิ่มเติม ต้องทดสอบกับข้อมูลที่ไม่ได้ใช้เลือกวิธีด้วย ไม่ควรเลือกมาตรการจากผลย้อนหลังชุดเดียวแล้วเรียกว่าปัญหาค่าประมาณหมดไป
การไม่ใช้ mean ยังเหลือปัญหา covariance
GMV มักเป็นทางเลือกเมื่อไม่อยากให้ forecast ผลตอบแทนเป็นตัวกำหนดน้ำหนัก อย่างไรก็ตาม หาก sample covariance ประเมินความผันผวนของชื่อหนึ่งต่ำเกินจริง GMV อาจทุ่มน้ำหนักไปที่ชื่อนั้น และถ้าประเมิน correlation ผิด การกระจายความเสี่ยงที่เห็นในแบบจำลองอาจไม่เกิดตามนั้น
สำหรับ สินทรัพย์ covariance มีตัวเลขไม่ซ้ำ ค่า เมื่อมีจำนวนเดือนน้อยเมื่อเทียบกับจำนวนสินทรัพย์ การประมาณหลายค่าพร้อมกันทำได้ยากขึ้น ถ้าตัวอย่างมี แถว sample covariance ที่หัก mean แล้วมี rank ไม่เกิน ดังนั้นกรณี จะไม่สามารถเป็น matrix เต็ม rank ได้ ปัญหาการระบุน้ำหนักจากข้อมูลจึงอาจตามมา
ก่อน optimize ควรตรวจความถี่ วันเริ่มและสิ้นสุด การปรับปันผล หน่วยสกุลเงิน ข้อมูลหาย และเหตุผลที่เลือกช่วงประมาณค่า ช่วงสั้นตอบสนองต่อข้อมูลใหม่เร็วแต่มี sample น้อย ช่วงยาวมีข้อมูลเพิ่มแต่เสี่ยงรวมหลายสภาวะที่ไม่เหมือนปัจจุบัน ไม่มีความยาวหน้าต่างที่ใช้ได้ดีที่สุดกับทุกสินทรัพย์โดยไม่ต้องทดสอบ
วิธีเช่น covariance shrinkage ใช้ค่าประมาณจาก sample ร่วมกับโครงสร้างเป้าหมายเพื่อลดความแกว่งของค่าประมาณ ความแม่นของ covariance และผลลัพธ์พอร์ตเป็นคำถามที่ต้องตรวจแยก การทำให้ matrix คำนวณง่ายขึ้นไม่ได้รับรองว่า forecast ดีขึ้นโดยอัตโนมัติ
แยกข้อมูลที่ใช้เลือกน้ำหนักจากข้อมูลที่ใช้วัดผล
การคำนวณ Sharpe สูงสุดจาก sample แล้ววัด Sharpe บน sample เดิมเป็นผล in sample ตัวเลือกถูกปรับให้เข้ากับข้อมูลที่กำลังใช้ให้คะแนนอยู่แล้ว หากต้องการตรวจว่าแนวทางทำงานหลังตัดสินใจได้หรือไม่ ต้องมีช่วง out of sample ที่ยังไม่ถูกใช้เลือกน้ำหนัก
สร้างข้อมูลสมมติ 12 เดือน แบ่งหกเดือนแรกเป็น train เพื่อประมาณ covariance แล้วให้ GMV ใช้น้ำหนักนั้นลงทุนตั้งแต่เดือนที่เจ็ด เราจะถือสินทรัพย์แต่ละตัวต่อโดยไม่รีบาลานซ์ภายในหกเดือนทดสอบ จึงปล่อยให้น้ำหนักเปลี่ยนตามมูลค่า ตัวอย่างเล็กนี้มีไว้ตรวจลำดับการคำนวณ ไม่ใช้จัดอันดับกลยุทธ์ในตลาด
history = pd.DataFrame({
"A": [0.01, -0.02, 0.02, 0.00, 0.015, -0.005, 0.01, 0.005, -0.015, 0.02, 0.00, 0.01],
"B": [0.03, -0.04, 0.01, 0.02, -0.01, 0.04, -0.02, 0.03, 0.01, -0.025, 0.035, 0.015],
"C": [0.05, -0.06, 0.03, -0.02, 0.04, 0.01, 0.06, -0.05, 0.025, 0.04, -0.03, 0.02]
}, index=[f"M{i}" for i in range(1, 13)])
train = history.iloc[:6]
test = history.iloc[6:]
fitted_cov = train.cov(ddof=1).to_numpy()
fitted_gmv = find_gmv(fitted_cov)
print(pd.Series(fitted_gmv, index=history.columns).round(6))
.iloc[:6] เลือกตำแหน่ง 0 ถึง 5 ส่วน .iloc[6:] เริ่มตำแหน่ง 6 ไปจนจบ [f"M{i}" for i in range(1, 13)] สร้างชื่อ M1 ถึง M12 ด้วยการวนตัวเลข 1 ถึง 12; จุดสิ้นสุด 13 ของ range ไม่ถูกรวมในผลลัพธ์ .to_numpy() เอาป้ายชื่อออก จึงต้องรักษาลำดับ A/B/C ที่ใช้ตลอดการคำนวณ
น้ำหนัก GMV ที่ fit จาก train คือ A ประมาณ 92.1053%, B 7.8947%, C 0% แม้จะตั้งใจลด variance ก็ยังเกิดการกระจุกตัวได้ การเห็นคำว่า minimum variance จึงยังไม่บอกว่ากระจายเงินเท่ากันหรือมีความเสี่ยงครบทุกด้านต่ำ
คำนวณเส้นทางเงินแบบ buy and hold ให้ตรงกับนโยบาย
.cumprod() คูณสะสม gross return ลงไปตามเดือนของแต่ละสินทรัพย์ เช่นค่าของ A เดือนที่แปดคือ จากนั้นถ่วงด้วยน้ำหนักที่เลือกไว้เมื่อเริ่มช่วงทดสอบแล้วรวมมูลค่า วิธีนี้คิดการถือสินทรัพย์เดิม โดยไม่บังคับน้ำหนักกลับสู่เป้าทุกเดือน
asset_growth = (1 + test).cumprod()
start_gmv = pd.Series(fitted_gmv, index=test.columns)
start_ew = pd.Series(1 / 3, index=test.columns)
gmv_wealth = asset_growth.mul(start_gmv, axis="columns").sum(axis=1)
ew_wealth = asset_growth.mul(start_ew, axis="columns").sum(axis=1)
holdout_wealth = pd.DataFrame({"GMV": gmv_wealth, "EW": ew_wealth})
print(holdout_wealth.round(6))
print(((holdout_wealth.iloc[-1] - 1) * 100).round(4))
กำหนดมูลค่าตอนเริ่ม M7 เป็น 1 หน่วย เมื่อจบ M12 ได้ GMV ประมาณ 1.031141 และ EW 1.045444 หรือผลตอบแทนสะสมประมาณ 3.1141% กับ 4.5444% ในหกเดือน ตามข้อมูลสมมติชุดนี้ EW ได้มากกว่า แต่ยังสรุปว่า EW เหนือกว่า GMV ในอนาคตไม่ได้ การเลือกช่วงหกเดือนหรือออกแบบตัวเลขอีกชุดย่อมเปลี่ยนผลได้
ข้อมูลที่ใช้เลือกน้ำหนักของ GMV จบที่ M6 และใช้จริงเริ่ม M7 ถ้าเลือกน้ำหนักด้วย covariance ที่รวม M7 ถึง M12 แล้วนำไปอ้างผลตั้งแต่ M7 จะเกิด look-ahead bias เพราะใช้ข้อมูลอนาคตย้อนกลับมาตัดสินใจในอดีต
สำหรับการประเมินหลายช่วง อาจเดินหน้าต่าง train ไปทีละเดือน เลือกน้ำหนักจากข้อมูลถึงสิ้นเดือนก่อนหน้า แล้วใช้กับเดือนถัดไป แต่ต้องกำหนดเวลาเผยแพร่ข้อมูล จังหวะส่งคำสั่ง และวันที่ราคาที่ซื้อขายได้ให้สอดคล้องกันด้วย ผลตอบแทนที่เผยแพร่หลังปิดงวดใช้ตัดสินใจที่ต้นงวดเดิมไม่ได้
เพิ่มต้นทุนตามจำนวนที่ซื้อขายจริง
ตัวอย่าง train/test ยังไม่หักต้นทุน หากนำไปเปรียบเทียบกลยุทธ์ที่รีบาลานซ์บ่อย ต้องตรวจน้ำหนักก่อนซื้อขายซึ่งเปลี่ยนตามราคาแล้ว เทียบกับเป้าหมายใหม่ ไม่ควรใช้เพียงผลต่างระหว่างน้ำหนักเป้าหมายของเดือนติดกัน เพราะอาจมีการซื้อขายแม้เป้าหมายสองเดือนเท่ากัน
เช่นพอร์ต 60/40 จากบทก่อนเคลื่อนไปเป็น 63.4615/36.5385 ต้องขาย A 3.4615% ของมูลค่าพอร์ตและซื้อ B 3.4615% เพื่อกลับสู่เป้า ถ้าคิดค่าธรรมเนียม 0.1% ของมูลค่าที่ซื้อหรือขายแต่ละขา มูลค่าซื้อขายรวมคือ 6.9231% ของพอร์ต ต้นทุนจึงประมาณ 0.006923% ของพอร์ตก่อนปรับผลของค่าธรรมเนียมต่อมูลค่าที่ใช้คำนวณเป้า
บางรายงานเรียก turnover ว่าครึ่งหนึ่งของผลรวมการเปลี่ยนน้ำหนักสัมบูรณ์ บางรายงานใช้ทั้งผลรวม ต้องระบุนิยามก่อนคูณอัตราต้นทุน นอกจากนี้ spread, slippage, ภาษีและผลกระทบจากคำสั่งขนาดใหญ่ยังอาจทำให้ต้นทุนจริงต่างจากค่าธรรมเนียมที่ตั้งไว้
การเลือกหุ้นจากรายชื่อที่ยังอยู่รอดในปัจจุบันแล้วทดสอบย้อนหลังทำให้เกิด survivorship bias ได้ ควรใช้รายการสินทรัพย์ที่รู้จักและเข้าถึงได้จริง ณ เวลานั้น รวมข้อมูลเพิกถอนหรือข้อจำกัดการซื้อขายที่เกี่ยวข้อง แยกช่วงพัฒนากฎออกจากช่วงประเมิน และบันทึกจำนวนทางเลือกที่ทดลองเพื่อไม่เลือกเฉพาะผลดีที่สุดมาเล่า
แบบฝึกหัดพร้อมเฉลย
1. ถือ MSR 50% และ risk-free 50% น้ำหนัก A/B/C ในพอร์ตรวมเป็นเท่าไร?
ดูเฉลย: คูณน้ำหนักภายในด้วยสัดส่วนสินทรัพย์เสี่ยง
น้ำหนัก A ประมาณ 19.1992%, B 14.9897%, C 15.8111% และ risk-free 50% รวมกันเป็น 100% ตัวเลข 38.3984/29.9795/31.6222 ของ MSR เป็นสัดส่วนภายในก้อนเสี่ยง ก่อนนำไปคูณด้วยสัดส่วนก้อนเสี่ยงในพอร์ตรวม
2. เลือก GMV แล้วจึงไม่ต้องประมาณอะไรอีก จริงหรือไม่?
ดูเฉลย: GMV ตัด expected return ออกจาก objective
ยังต้องใช้ covariance ซึ่งขึ้นกับความผันผวนและการเคลื่อนไหวร่วมกัน ต้องเลือกรายการสินทรัพย์ หน้าต่างข้อมูล ข้อจำกัด และนโยบายปรับพอร์ตด้วย GMV เลี่ยงการประมาณ mean เพื่อหาน้ำหนัก แต่ไม่ได้เลี่ยงความคลาดเคลื่อนของทุกข้อมูลเข้า
3. หากเลือกวิธี shrinkage ที่ได้ผลดีที่สุดบนช่วง test แล้วรายงาน test เดิมเป็นผลอิสระ จะเกิดปัญหาอะไร?
ดูเฉลย: ช่วงที่ใช้เลือกกลายเป็นส่วนของการพัฒนาแล้ว
ข้อมูล test ถูกใช้เลือกวิธีไปแล้ว จึงมีโอกาสปรับกฎเข้ากับ noise ของช่วงนั้น ควรแยก validation สำหรับเลือกวิธีและเก็บช่วงประเมินสุดท้ายที่ยังไม่ถูกใช้ตัดสินใจ หรือใช้กระบวนการเดินหน้าที่กำหนดไว้ล่วงหน้า โดยไม่ปรับย้อนหลังตามผลที่เห็น
4. ทำไมพอร์ตที่ mean สูงสุดไม่จำเป็นต้องเป็น MSR?
ดูเฉลย: คำนวณส่วนเพิ่มต่อความผันผวน
C เดี่ยวมี mean 14%, SD 20%, risk-free 2% จึงได้ Sharpe ส่วน MSR สามสินทรัพย์มี mean ต่ำกว่าที่ประมาณ 9.7290% แต่ SD 10.1879% และ Sharpe ประมาณ 0.758640 จึงให้ส่วนเพิ่มเหนือ risk-free ต่อหนึ่งหน่วย SD สูงกว่าในแบบจำลองนี้
แหล่งอ้างอิงและอ่านต่อ
- EDHEC Business School, Fund Separation Theorem and the Capital Market Line, Lack of Robustness of Markowitz Analysis, Lab: Maximum Sharpe Ratio และ Lab: EW and GMV. อ่านประกอบ Lab 110–111 แล้วสร้างข้อมูลและโค้ดของบทนี้ใหม่
- William F. Sharpe, The Sharpe Ratio, 1994. นิยามจาก mean และ SD ของ differential return และข้อพิจารณาเรื่องความถี่เวลา
- SciPy: SLSQP สำหรับ constraints และสถานะผลลัพธ์ของตัวแก้ที่ใช้
เมื่อต้องการนำสูตรไปทำงานกับข้อมูลจริง ให้เริ่มจากระบุวันที่เลือกน้ำหนัก วันแรกที่นำไปลงทุน และนโยบายถือหรือรีบาลานซ์ในรายงานเดียวกับผลตอบแทน เพื่อให้ตรวจการตัดสินใจย้อนหลังได้