Portfolio Management Toolkit Notes
Portfolio Management Toolkit Notes

การสร้างพอร์ต · บทย่อย 1

เริ่มจัดพอร์ตจากสินทรัพย์สองตัว

ถ้าหุ้น A ผันผวน 20% และสินทรัพย์ B ผันผวน 10% การแบ่งเงินลงทุนจะทำให้พอร์ตผันผวนเท่าไร?

คำตอบขึ้นอยู่กับทั้งสัดส่วนเงินที่ใส่และการเคลื่อนไหวของสินทรัพย์สองตัวในช่วงเดียวกัน เราจะเริ่มจากเงิน 10,000 บาท คำนวณผลตอบแทนเป็นบาทก่อน แล้วค่อยดูว่าทำไมการนำความผันผวนมาเฉลี่ยตรง ๆ จึงมักไม่ตรงกับความผันผวนของพอร์ต

เนื้อหานี้เรียบเรียงประกอบหัวข้อการกระจายการลงทุนและ Lab 107–108 ตัวเลขทั้งหมดเป็นตัวอย่างสมมติ โค้ดสร้างข้อมูลเองและรันได้โดยไม่ใช้ CSV ของคอร์ส อ่านพื้นฐานผลตอบแทนและ Python ก่อนหากยังไม่คุ้นกับตัวแปร Series และ DataFrame

ตัวอย่างช่วงแรกใช้ผลตอบแทนรายเดือนที่เกิดขึ้นแล้วในสถานการณ์สมมติ ส่วนช่วงท้ายใช้ expected return และ covariance ต่อปีที่ตั้งเป็นข้อมูลเข้าแบบจำลอง เราจะแยกชื่อชุดข้อมูลให้ชัด และไม่ถือว่าค่าเฉลี่ยจากข้อมูลไม่กี่เดือนเป็นผลตอบแทนในอนาคตที่ทราบแน่นอน

ดาวน์โหลด Notebook ของบทนี้ เพื่อรันตัวอย่างตามลำดับและเทียบผลลัพธ์ที่บันทึกไว้

น้ำหนักพอร์ตคือสัดส่วนเงินต้นช่วง

สมมติว่ามีเงิน 10,000 บาท ซื้อ A มูลค่า 6,000 บาทและ B มูลค่า 4,000 บาท น้ำหนักของ A คือ wA=6,000/10,000=0.6w_A=6{,}000/10{,}000=0.6 และน้ำหนักของ B คือ wB=0.4w_B=0.4 ตัว ww ย่อจาก weight หน่วยเป็นสัดส่วนของมูลค่าพอร์ต ไม่ใช่จำนวนหุ้น

ถ้า A ราคาหุ้นละ 200 บาทและ B หุ้นละ 20 บาท เราจะถือ A 30 หุ้นและ B 200 หุ้น จำนวนหุ้น B มากกว่าไม่ได้แปลว่ามีน้ำหนักมากกว่า ต้องคูณจำนวนหุ้นด้วยราคาก่อนแล้วจึงเทียบมูลค่า

สินทรัพย์ เงินลงทุนต้นเดือน น้ำหนัก ผลตอบแทนเดือนนั้น มูลค่าปลายเดือน
A 6,000 บาท 60% +10% 6,600 บาท
B 4,000 บาท 40% −5% 3,800 บาท
รวม 10,000 บาท 100% +4% 10,400 บาท

พอร์ตได้กำไร 400 บาทเทียบเงินต้น 10,000 บาท ผลตอบแทนจึงเป็น 4% อีกวิธีหนึ่งคือถ่วงน้ำหนักผลตอบแทนของแต่ละตัวด้วยน้ำหนักต้นเดือน:

Rp=wARA+wBRB=0.6(0.10)+0.4(−0.05)=0.04.R_p=w_AR_A+w_BR_B =0.6(0.10)+0.4(-0.05)=0.04.

ตัวห้อย pp หมายถึง portfolio หรือพอร์ต ตัวเลขผลตอบแทนในสูตรต้องเป็นทศนิยม เช่น 10% ใช้ 0.10 ผลตอบแทนแต่ละตัวต้องครอบคลุมช่วงเวลาเดียวกันและนับปันผลหรือกระแสเงินสดอย่างสอดคล้องกัน ตัวอย่างนี้ไม่มีฝากถอน ภาษี ค่าธรรมเนียม หรือซื้อขายระหว่างเดือน

เปิด Notebook ใหม่แล้วรันช่องตามลำดับ pd.Series เก็บค่าพร้อมชื่อ A/B และ .sum() รวมค่าทั้งชุด การคูณ Series จะจับคู่ตามชื่อสินทรัพย์

import numpy as np
import pandas as pd

start_values = pd.Series({"A": 6000.0, "B": 4000.0})
one_month = pd.Series({"A": 0.10, "B": -0.05})
weights = start_values / start_values.sum()
end_values = start_values * (1 + one_month)
portfolio_return = (weights * one_month).sum()
print(f"Portfolio return: {portfolio_return:.2%}")
print(f"End value: {end_values.sum():,.2f}")

ได้ 4.00% และ 10,400.00 รูปแบบ :.2% แสดงทศนิยมเป็นเปอร์เซ็นต์สองตำแหน่ง ส่วน :,.2f แสดงจำนวนเงินโดยใส่จุลภาคคั่นหลักพันและทศนิยมสองตำแหน่ง ค่าที่ใช้คำนวณยังคงเป็นตัวเลขเดิม

น้ำหนักรวมหนึ่ง และความหมายของการขายชอร์ต

ในบทนี้เริ่มจากข้อกำหนด wA+wB=1w_A+w_B=1 และ wA,wB≥0w_A,w_B\geq0 เรียกว่า fully invested, long-only: ใช้เงินทั้งหมดกับสินทรัพย์ที่อยู่ในรายการและไม่มีสถานะขายชอร์ต น้ำหนักศูนย์ทำได้ แปลว่าไม่ได้ถือสินทรัพย์นั้น หากมีเงินสดต้องใส่เงินสดเป็นอีกสินทรัพย์หนึ่ง หรือระบุแยกว่ารายการที่กำลังพิจารณาเป็นเพียงส่วนหนึ่งของพอร์ต

น้ำหนัก 1.21.2 กับ −0.2-0.2 รวมเป็นหนึ่งเช่นกัน แต่ความหมายเปลี่ยนไป เราถือสินทรัพย์แรก 120% ของเงินทุนและมีสถานะ short สินทรัพย์ที่สอง 20% การขายชอร์ตต้องมีเงื่อนไขการยืม หลักประกัน และต้นทุนเพิ่มเติม ข้อกำหนดน้ำหนักรวมหนึ่งเพียงข้อเดียวจึงยังไม่ได้ห้ามการลงทุนเกินเงินทุน

เดือนถัดไปน้ำหนักเปลี่ยน แม้เราไม่ได้ซื้อขาย

จากตารางเมื่อสิ้นเดือน A มีมูลค่า 6,600 บาทและ B 3,800 บาท น้ำหนักใหม่ของ A จึงเป็น 6,600/10,400≈63.46%6{,}600/10{,}400\approx63.46\% และ B ประมาณ 36.54% สินทรัพย์ที่ขึ้นมากกว่าจะกินสัดส่วนของพอร์ตเพิ่มเอง

drifted_weights = end_values / end_values.sum()
next_returns = pd.Series({"A": -0.10, "B": 0.05})
next_buy_hold = (drifted_weights * next_returns).sum()
next_rebalanced = (weights * next_returns).sum()
print((drifted_weights * 100).round(4))
print(f"Next month, buy and hold: {next_buy_hold:.4%}")
print(f"Next month, reset to 60/40: {next_rebalanced:.4%}")

น้ำหนักกลายเป็น A 63.4615% และ B 36.5385% ถ้าเดือนถัดไป A ได้ −10% และ B +5% การถือเดิมให้ผลตอบแทนเดือนนั้น −4.5192% ส่วนการขายบางส่วนของ A แล้วซื้อ B กลับไป 60/40 ก่อนเริ่มเดือนให้ −4.0000% ก่อนต้นทุนซื้อขาย

การคำนวณ 0.6RA+0.4RB0.6R_A+0.4R_B ทุกเดือนจึงแฝงสมมติฐานว่าปรับพอร์ตกลับสู่น้ำหนักเดิมทุกต้นเดือน หรือ rebalancing หากตั้งใจจำลอง buy and hold ต้องปล่อยให้น้ำหนักเปลี่ยนตามมูลค่า ห้ามนำสองนโยบายมาปนกันเพราะชื่อพอร์ตตอนเริ่มเหมือนกัน

ความผันผวนต้องดูผลตอบแทนที่เกิดในเดือนเดียวกัน

เปลี่ยนมาใช้ข้อมูลสมมติอีกชุดสี่เดือน เพื่อคำนวณความเสี่ยงด้วยมือได้ครบทุกแถว แต่ละคอลัมน์คือสินทรัพย์ แต่ละแถวคือเดือนเดียวกันสำหรับทั้ง A และ B

monthly = pd.DataFrame({
    "A": [0.02, 0.04, -0.02, 0.00],
    "B": [0.01, 0.00, 0.03, 0.00]
}, index=["M1", "M2", "M3", "M4"])
monthly_weights = pd.Series({"A": 0.60, "B": 0.40})
monthly_portfolio = monthly.mul(monthly_weights, axis="columns").sum(axis=1)
print((monthly * 100).round(2))
print((monthly_portfolio * 100).round(2))

.mul(..., axis="columns") ให้คูณน้ำหนักตามชื่อคอลัมน์ และ .sum(axis=1) รวมข้ามคอลัมน์ในแต่ละแถว จึงได้ผลตอบแทนพอร์ต M1 ถึง M4 เป็น 1.6%, 2.4%, 0%, 0% เราถือว่ารีบาลานซ์กลับ 60/40 ทุกต้นเดือนและไม่มีต้นทุน

A และ B มีค่าเฉลี่ย 1% ต่อเดือนเหมือนกัน แต่ A ขึ้นลงมากกว่า เมื่อใช้ sample SD ซึ่งหาร variance ด้วย n−1=3n-1=3 จะได้ SD ของ A ประมาณ 2.5820% และ B ประมาณ 1.4142% ต่อเดือน

ถ้าเอา SD มาถ่วงน้ำหนักตรง ๆ จะได้ 0.6(2.5820%)+0.4(1.4142%)≈2.1149%0.6(2.5820\%)+0.4(1.4142\%)\approx2.1149\% แต่ SD ที่คำนวณจากผลตอบแทนพอร์ตสี่เดือนจริงได้เพียง 1.2% ต่อเดือน ส่วนต่างเกิดจากการเคลื่อนไหวร่วมกันของสินทรัพย์: ในบางเดือนกำไรของตัวหนึ่งช่วยหักล้างขาดทุนของอีกตัว

Covariance เริ่มจากผลคูณของส่วนต่างจากค่าเฉลี่ย

เอาผลตอบแทนแต่ละเดือนลบค่าเฉลี่ยของสินทรัพย์นั้นก่อน ส่วนต่างที่เป็นบวกหมายถึงเดือนนั้นดีกว่าค่าเฉลี่ยของตัวเอง ส่วนต่างติดลบหมายถึงต่ำกว่าค่าเฉลี่ย ไม่จำเป็นต้องขาดทุนเป็นบาท

เดือน RA−RˉAR_A-\bar R_A RB−RˉBR_B-\bar R_B ผลคูณส่วนต่าง
M1 0.01 0.00 0.0000
M2 0.03 −0.01 −0.0003
M3 −0.03 0.02 −0.0006
M4 −0.01 −0.01 0.0001
รวม 0 0 −0.0008

ถ้าส่วนต่างทั้งคู่มีเครื่องหมายเดียวกัน ผลคูณเป็นบวก หากเครื่องหมายตรงข้าม ผลคูณเป็นลบ sample covariance จึงเท่ากับผลรวมดังกล่าวหารด้วย n−1n-1:

sAB=1n−1∑t=1n(RA,t−RˉA)(RB,t−RˉB)=−0.00083≈−0.000266667.s_{AB}=\frac{1}{n-1}\sum_{t=1}^n (R_{A,t}-\bar R_A)(R_{B,t}-\bar R_B) =\frac{-0.0008}{3}\approx-0.000266667.

ตัว tt บอกเดือน ส่วน RˉA\bar R_A และ RˉB\bar R_B คือค่าเฉลี่ยของแต่ละคอลัมน์ Covariance มีหน่วยผลตอบแทนยกกำลังสอง เมื่อข้อมูลเป็นทศนิยม อย่าอ่าน −0.000266667 ว่าเป็นผลตอบแทนขาดทุน −0.0267% มันเป็นตัววัดการเคลื่อนไหวร่วมกัน

centered = monthly - monthly.mean()
manual_covariance = (centered["A"] * centered["B"]).sum() / (len(monthly) - 1)
cov_monthly = monthly.cov(ddof=1)
print(f"Manual covariance: {manual_covariance:.9f}")
print(cov_monthly.round(9))

ตาราง covariance ที่ได้มีลักษณะนี้:

Σ^m=[0.000666667−0.000266667−0.0002666670.000200000].\widehat\Sigma_m= \begin{bmatrix} 0.000666667 & -0.000266667\\ -0.000266667 & 0.000200000 \end{bmatrix}.

แนวทแยง A/A และ B/B คือ variance ของสินทรัพย์นั้น เพราะเป็นการคูณส่วนต่างกับตัวเอง ช่อง A/B กับ B/A เท่ากันเพราะสลับลำดับการคูณได้ เมื่อนำ covariance ของทุกคู่มาเรียงเป็นตาราง เราเรียกตารางนี้ว่า covariance matrix เครื่องหมายหมวกบน Σ^\widehat\Sigma เตือนว่านี่เป็นค่าประมาณจากตัวอย่าง

DataFrame.cov() ใช้ข้อมูลที่จับคู่ได้ในแต่ละคู่คอลัมน์ หากมีข้อมูลหาย จำนวนเดือนของแต่ละคู่จึงอาจต่างกัน การใช้ covariance matrix ที่สร้างจากคนละช่วงอาจทำให้คุณสมบัติที่ต้องการสำหรับการจัดพอร์ตเสียไป ในบทนี้ข้อมูลครบทุกแถว จึงใช้สี่เดือนเดียวกันทั้งหมด pandas: DataFrame.cov

Correlation ทำให้ความสัมพันธ์ไม่มีหน่วย

Covariance ขึ้นกับขนาดความผันผวนของทั้งสองตัว เพื่ออ่านความสัมพันธ์บนมาตราส่วนเดียวกัน เราหารด้วย SD ของ A คูณ SD ของ B:

ρAB=sABsAsB.\rho_{AB}=\frac{s_{AB}}{s_As_B}.

อักษรกรีก ρ\rho อ่านว่า “โร” ค่าอยู่ระหว่าง −1 ถึง +1 เมื่อ SD ทั้งสองเป็นบวก จากตัวอย่างจะได้

ρAB=−0.0002666670.025819889×0.014142136≈−0.730297.\rho_{AB} =\frac{-0.000266667}{0.025819889\times0.014142136} \approx-0.730297.
asset_sd = monthly.std(ddof=1)
manual_correlation = manual_covariance / (asset_sd["A"] * asset_sd["B"])
print(f"Correlation: {manual_correlation:.6f}")
print(monthly.corr().round(6))

Correlation −0.730297 บอกว่าในตัวอย่างนี้ส่วนต่างจากค่าเฉลี่ยมีความสัมพันธ์เชิงเส้นทางลบ ไม่มีหน่วยเป็นรายเดือนหรือรายปีในตัวเลขเอง แต่ต้องบอกว่าคำนวณจากข้อมูลความถี่ไหนและช่วงใด เพราะเปลี่ยนวิธีรวมข้อมูลแล้วค่าประมาณอาจเปลี่ยน

Correlation +1 หมายถึงสัมพันธ์เชิงเส้นทางบวกสมบูรณ์ ส่วน −1 หมายถึงเชิงเส้นทางลบสมบูรณ์ ค่า 0 หมายถึงไม่มีความสัมพันธ์เชิงเส้นตามตัววัดนี้ ยังไม่เพียงพอที่จะบอกว่าเป็นอิสระกัน ความสัมพันธ์ที่โค้งหรือเปลี่ยนตามสภาวะอาจยังมีอยู่ได้ และ correlation ติดลบจากข้อมูลสี่เดือนไม่รับประกันว่าจะยังติดลบในวิกฤตครั้งถัดไป

ตัวอย่างสั้นที่ correlation ศูนย์แต่ยังสัมพันธ์กัน

ให้ A มีค่า −1, 0, 1 โดยมีโอกาสเท่ากัน และกำหนด B เท่ากับ A ยกกำลังสอง ดังนั้น B เป็น 1, 0, 1 ถ้ารู้ A จะรู้ B ทันที จึงไม่เป็นอิสระ แต่ correlation เท่ากับศูนย์เพราะผลคูณส่วนต่างทางซ้ายและขวาหักล้างกัน ตัวอย่างนี้เป็นตัวเลขสาธิตความสัมพันธ์ ไม่ใช่ผลตอบแทนการลงทุน

x = np.array([-1.0, 0.0, 1.0])
y = x ** 2
print(np.corrcoef(x, y).round(6))

ช่องนอกแนวทแยงของตารางเป็นศูนย์ การเขียน ** 2 หมายถึงยกกำลังสองทุกสมาชิก และ np.corrcoef คืนตาราง correlation ของตัวแปรที่ส่งเข้าไป

คำนวณ variance ของพอร์ตสองสินทรัพย์

ผลตอบแทนพอร์ตเป็นผลบวกถ่วงน้ำหนัก แต่เมื่อยกผลบวกนั้นกำลังสอง จะเกิดพจน์ผลคูณไขว้ด้วย สูตร variance จึงเป็น

σp2=wA2σA2+wB2σB2+2wAwBCov⁡(RA,RB).\sigma_p^2 =w_A^2\sigma_A^2+w_B^2\sigma_B^2 +2w_Aw_B\operatorname{Cov}(R_A,R_B).

ใช้ความสัมพันธ์ Cov⁡(RA,RB)=ρABσAσB\operatorname{Cov}(R_A,R_B)=\rho_{AB}\sigma_A\sigma_B จะได้อีกแบบหนึ่ง:

σp=wA2σA2+wB2σB2+2wAwBρABσAσB.\sigma_p=\sqrt{ w_A^2\sigma_A^2+w_B^2\sigma_B^2 +2w_Aw_B\rho_{AB}\sigma_A\sigma_B}.

สองพจน์แรกคิดความผันผวนของแต่ละส่วนที่ถือ ส่วนพจน์สุดท้ายคิดว่าทั้งสองเคลื่อนร่วมกันอย่างไร หาก covariance ติดลบ พจน์นั้นช่วยลด variance ของผลรวม เราต้องคำนวณ variance ก่อนแล้วค่อยถอดรากที่สองกลับเป็น SD CFA Institute: Portfolio Mathematics

ลองตรวจสูตรกับข้อมูลสี่เดือน โดย .loc["A", "B"] เลือก covariance แถว A คอลัมน์ B ตามชื่อ

wa, wb = 0.60, 0.40
variance_by_formula = (
    wa ** 2 * cov_monthly.loc["A", "A"]
    + wb ** 2 * cov_monthly.loc["B", "B"]
    + 2 * wa * wb * cov_monthly.loc["A", "B"]
)
sd_by_formula = np.sqrt(variance_by_formula)
sd_from_returns = monthly_portfolio.std(ddof=1)
print(f"Formula: {sd_by_formula:.4%}")
print(f"Direct calculation: {sd_from_returns:.4%}")
print(np.isclose(sd_by_formula, sd_from_returns))

ทั้งสองวิธีได้ 1.2000% ต่อเดือน และ True จาก np.isclose ซึ่งเทียบตัวเลขภายในความคลาดเคลื่อนทศนิยมของคอมพิวเตอร์ การคำนวณจากผลตอบแทนพอร์ตโดยตรงจึงใช้ตรวจคำตอบจากสูตร covariance ได้

วางหน่วยเวลาให้ตรงกันก่อนเปรียบเทียบพอร์ต

ในแบบจำลองหนึ่งช่วงเวลา expected return และ covariance ต้องหมายถึงช่วงเดียวกัน หากแกนนอนเป็น volatility ต่อเดือนแต่แกนตั้งเป็น expected return ต่อปี ต้องเปลี่ยนให้สอดคล้องกันก่อนตีความความชันหรือคำนวณ Sharpe ratio

ภายใต้สมมติฐานว่าผลตอบแทนรายเดือนไม่มี covariance ข้ามเวลาและความเสี่ยงต่อเดือนคงที่ variance ของ ผลรวม สิบสองเดือนเท่ากับ 12 เท่าของ variance รายเดือน ดังนั้น convention annualization ที่ใช้ทั่วไปคือ

Σ^ann=12Σ^m,σ^ann=12σ^m.\widehat\Sigma_{\mathrm{ann}}=12\widehat\Sigma_m, \qquad \widehat\sigma_{\mathrm{ann}}=\sqrt{12}\widehat\sigma_m.

สำหรับ simple returns ที่ต้องทบต้น ผลตอบแทนหนึ่งปีเป็นผลคูณของ gross returns จึงไม่ได้มี variance เท่ากับสูตรผลรวมนี้อย่างแม่นยำเสมอ การคูณ 12 และ 12\sqrt{12} เป็น convention หรือการประมาณที่ต้องบอกสมมติฐาน หากมี autocorrelation ต้องนับ covariance ข้ามเวลาด้วย

ค่าเฉลี่ยเลขคณิตรายเดือนคูณ 12 ใช้แสดง mean ใน convention รายปีได้ ส่วน CAGR ตอบการเติบโตแบบทบต้นที่เกิดขึ้นจริง และไม่ได้เป็นตัวเดียวกับ expected arithmetic return ในสูตร μp=∑iwiμi\mu_p=\sum_iw_i\mu_i การเอา CAGR ของแต่ละสินทรัพย์มาถ่วงน้ำหนักจึงไม่รับประกันว่าจะได้ CAGR ของพอร์ต

mean_annual_convention = monthly.mean() * 12
cov_annual_convention = cov_monthly * 12
sd_annual_convention = monthly.std(ddof=1) * np.sqrt(12)
portfolio_sd_annual = monthly_portfolio.std(ddof=1) * np.sqrt(12)
print((mean_annual_convention * 100).round(4))
print((sd_annual_convention * 100).round(4))
print(f"Portfolio annualized SD: {portfolio_sd_annual:.4%}")

ค่าเฉลี่ยตาม convention เท่ากับ 12% ต่อปีทั้งสองตัว SD ของ A ประมาณ 8.9443% และ B 4.8990% ต่อปี ส่วนพอร์ต 4.1569% ตัวเลขเหล่านี้แปลงจากตัวอย่างเพียงสี่เดือนเพื่อฝึกหน่วย ไม่มีข้อมูลครบหนึ่งปีหรือหลักฐานว่าพารามิเตอร์จะคงอยู่

ลองน้ำหนักหลายแบบบนแบบจำลองต่อปี

ตอนนี้เปลี่ยนจากข้อมูลสี่เดือนมาเป็น พารามิเตอร์สมมติต่อปี ชุดใหม่: A มี expected arithmetic return 8% และ SD 20%, B มี expected return 4% และ SD 10%, correlation เท่ากับ 0.25 เราใช้ตัวเลขเหล่านี้เป็นข้อมูลเข้า ไม่ได้ประมาณมาจากสี่เดือนด้านบน

พอร์ต 60/40 จึงมี expected return 0.6(8%)+0.4(4%)=6.4%0.6(8\%)+0.4(4\%)=6.4\% และ variance

0.62(0.20)2+0.42(0.10)2+2(0.6)(0.4)(0.25)(0.20)(0.10)=0.0184.0.6^2(0.20)^2+0.4^2(0.10)^2 +2(0.6)(0.4)(0.25)(0.20)(0.10)=0.0184.

เมื่อถอดรากได้ SD ประมาณ 13.5647% ต่อปี ซึ่งต่ำกว่า SD เฉลี่ยถ่วงน้ำหนัก 0.6(20%)+0.4(10%)=16%0.6(20\%)+0.4(10\%)=16\% เพราะ correlation ต่ำกว่า +1 แต่ยังสูงกว่า SD 10% ของ B การกระจายการลงทุนจึงไม่ได้แปลว่าทุกพอร์ตผสมต้องเสี่ยงต่ำกว่าทุกสินทรัพย์เดี่ยว

เก็บสูตรไว้ในฟังก์ชันด้วย def ส่วน return ส่งผลลัพธ์กลับให้ผู้เรียก ตัวแปร weight_a เป็นน้ำหนักของ A แล้วน้ำหนัก B เท่ากับ 1 - weight_a

mu_a, mu_b = 0.08, 0.04
sigma_a, sigma_b, rho_ab = 0.20, 0.10, 0.25

def two_asset_metrics(weight_a):
    weight_b = 1 - weight_a
    expected = weight_a * mu_a + weight_b * mu_b
    variance = (weight_a * sigma_a) ** 2 + (weight_b * sigma_b) ** 2
    variance += 2 * weight_a * weight_b * rho_ab * sigma_a * sigma_b
    return expected, np.sqrt(variance)

print(two_asset_metrics(0.60))

ได้ expected return 0.064 และ SD ประมาณ 0.1356466 การคืนค่าสองตัวคั่นด้วยจุลภาคทำให้รับกลับเป็นคู่ได้ += เป็นคำสั่งบวกค่าทางขวาเข้าไปในตัวแปรเดิม

สำหรับสองสินทรัพย์ เราลองน้ำหนักตั้งแต่ 0 ถึง 1 ได้โดยตรง np.linspace(0, 1, 5) สร้าง 0, 0.25, 0.5, 0.75 และ 1 ส่วน for ทำคำสั่งที่ย่อหน้าเข้าไปซ้ำกับแต่ละน้ำหนัก

candidate_rows = []
for weight_a in np.linspace(0, 1, 5):
    expected, volatility = two_asset_metrics(weight_a)
    candidate_rows.append({"Weight A": weight_a, "Mean": expected, "SD": volatility})
two_asset_table = pd.DataFrame(candidate_rows)
print((two_asset_table * 100).round(4))
น้ำหนัก A Expected return ต่อปี SD ต่อปี
0% 4.00% 10.0000%
25% 5.00% 10.0000%
50% 6.00% 12.2474%
75% 7.00% 15.8114%
100% 8.00% 20.0000%

ในชุดสมมตินี้พอร์ต A 25% / B 75% ให้ expected return 5% ที่ SD 10% ขณะที่ B ล้วนให้ expected return 4% ที่ SD 10% เท่ากัน หากตัดสินด้วย mean และ variance เพียงสองตัว พอร์ต B ล้วนจึงมีพอร์ตอีกแบบที่ดีกว่าโดยไม่เพิ่มความเสี่ยง

จุดที่ความผันผวนต่ำสุดอาจอยู่ระหว่างสินทรัพย์

น้ำหนัก A ที่ทำให้ variance ต่ำสุดสำหรับสองสินทรัพย์ โดยยังไม่บังคับขอบน้ำหนัก หาได้จาก

wAmin=σB2−ρABσAσBσA2+σB2−2ρABσAσB.w_A^{\mathrm{min}} =\frac{\sigma_B^2-\rho_{AB}\sigma_A\sigma_B} {\sigma_A^2+\sigma_B^2-2\rho_{AB}\sigma_A\sigma_B}.

สูตรนี้ได้จากหาจุดต่ำสุดของสมการกำลังสองใน wAw_A กรณีตัวอย่างได้ wA=0.125w_A=0.125 หรือ 12.5% และ B 87.5% ผลตอบแทนคาดหมายเท่ากับ 4.5% และ SD ประมาณ 9.6825% ต่อปี ต่ำกว่า B ล้วน หากสูตรให้น้ำหนักอยู่นอก 0 ถึง 1 แต่เราห้าม short ต้องหาจุดต่ำสุดภายในช่วงที่อนุญาตแทน

cov_ab = rho_ab * sigma_a * sigma_b
min_weight_a = (sigma_b ** 2 - cov_ab) / (sigma_a ** 2 + sigma_b ** 2 - 2 * cov_ab)
min_expected, min_sd = two_asset_metrics(min_weight_a)
print(f"Weight A: {min_weight_a:.2%}")
print(f"Expected return: {min_expected:.2%}; SD: {min_sd:.4%}")

เมื่อนำ SD วางบนแกนนอนและ expected return วางบนแกนตั้ง ทุกน้ำหนักเป็นจุดหนึ่งในพื้นที่ของพอร์ตที่สร้างได้ น้ำหนักบางแบบถูกพอร์ตอื่นครอบงำ ตัวอย่างเช่นจุดที่อยู่ใต้จุด variance ต่ำสุดอาจให้ผลตอบแทนน้อยลงพร้อมกับเสี่ยงมากขึ้น เราจะเรียกเฉพาะส่วนที่ไม่มีพอร์ตอื่นให้ mean สูงกว่าโดยไม่เพิ่ม variance ว่า efficient frontier ในบทถัดไป

ผลของ Correlation ต่อ SD ของพอร์ตสองสินทรัพย์
แกนนอนเป็นน้ำหนักของ A ตั้งแต่ 0% ถึง 100% แกนตั้งเป็น SD ต่อปี ใช้ SD ของ A 20% และ B 10% เหมือนตัวอย่าง แต่เปลี่ยน correlation เป็น +1, 0.25 และ −1 เส้นที่ต่ำกว่าหมายถึง variance ของผลรวมลดลงจากการเคลื่อนไหวร่วมกันที่ต่างไป ทุกเส้นเป็นแบบจำลองสมมติ

แบบฝึกหัดพร้อมเฉลย

1. พอร์ตเงิน 20,000 บาท ลงทุน A 5,000 บาทและ B 15,000 บาท เดือนเดียวกัน A ได้ +12% และ B ได้ −4% พอร์ตได้เท่าไร?

ดูเฉลย: เริ่มจากน้ำหนักต้นช่วง

น้ำหนักคือ 25% และ 75% ผลตอบแทนพอร์ตเท่ากับ 0.25(0.12)+0.75(−0.04)=00.25(0.12)+0.75(-0.04)=0 A กำไร 600 บาท และ B ขาดทุน 600 บาท เงินปลายเดือนยังเท่ากับ 20,000 บาท แต่น้ำหนัก A เพิ่มเป็น 5,600/20,000=28%5{,}600/20{,}000=28\% ก่อนเริ่มเดือนต่อไป

2. ใช้พอร์ต 60/40 ในแบบจำลอง A SD 20%, B SD 10% ถ้า correlation เปลี่ยนเป็น +1, 0 และ −1 จะได้ SD เท่าไร?

ดูเฉลย: เปลี่ยนเฉพาะพจน์ covariance

ได้ 16%, ประมาณ 12.6491% และ 8% ตามลำดับ ค่าต่ำสุดในสามกรณีนี้ยังไม่เป็นศูนย์ เพราะเมื่อ correlation −1 การหักล้างหมดต้องมี wAσA=wBσBw_A\sigma_A=w_B\sigma_B สำหรับ SD 20% และ 10% ต้องใช้น้ำหนัก A หนึ่งในสามและ B สองในสาม สมมติฐานสัมพันธ์เชิงเส้นทางลบสมบูรณ์และคงอยู่ตลอดเป็นเงื่อนไขของตัวอย่าง ไม่ใช่คำรับรองว่าจะสร้างพอร์ตไร้ความเสี่ยงในตลาดได้

3. มีราคาหุ้น A ทุกวัน แต่ราคาของ B มีเฉพาะสิ้นเดือน นำผลตอบแทนทั้งสองชุดมา .cov() ทันทีได้หรือไม่?

ดูเฉลย: จับช่วงเวลาให้ตรงก่อน

ต้องสร้างผลตอบแทนที่วัดช่วงเดียวกันก่อน เช่น เลือกราคาสิ้นเดือนของ A แล้วคำนวณผลตอบแทนรายเดือนให้ตรงกับ B ตรวจวันที่ หน่วยสกุลเงิน ปันผลและข้อมูลหายด้วย การจับค่ารายวันกับรายเดือนเพียงเพราะมีวันตรงกันบางแถวไม่ได้ทำให้ทั้งสองแทนผลตอบแทนช่วงเดียวกัน

แหล่งอ้างอิงและอ่านต่อ

ไปต่อที่ หลายสินทรัพย์และ Efficient Frontier เพื่อเปลี่ยนการลองน้ำหนักสองตัวเป็นปัญหาที่คอมพิวเตอร์ช่วยค้นหาได้

ค้นหาในหนังสือ