top of page
Portfolio Management Toolkit Notesสารบัญ
Portfolio Management Toolkit Notesพิมพ์หน้านี้

การสร้างพอร์ต · บทย่อย 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 ให้ผลตอบแทนที่ทราบแน่นอนสำหรับช่วงเวลาและหน่วยสกุลเงินที่กำลังพิจารณา สมมติหนึ่งปีได้ rf=2%r_f=2\% คงที่ จึงมี variance เป็นศูนย์และ covariance กับผลตอบแทนสินทรัพย์อื่นเป็นศูนย์ ตัวห้อย ff ใช้แทน risk-free

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

ให้พอร์ตสินทรัพย์เสี่ยงชุดหนึ่งมี expected return μP\mu_P และ SD σP\sigma_P ถ้าลงทุนสัดส่วน yy ในพอร์ตนั้น และเหลือ 1−y1-y ใน risk-free asset จะได้

RC=(1−y)rf+yRP,μC=rf+y(μP−rf),σC=∣y∣σP.R_C=(1-y)r_f+yR_P, \qquad \mu_C=r_f+y(\mu_P-r_f), \qquad \sigma_C=|y|\sigma_P.

CC ใช้แทนพอร์ตรวม ส่วนค่า absolute value ∣y∣|y| ทำให้ SD ไม่ติดลบ ช่วงแรกเราพิจารณา 0≤y≤10\leq y\leq1 จึงเป็นการแบ่งเงินที่มีระหว่างเงินปลอดความเสี่ยงกับพอร์ตเสี่ยง ค่า y=0.5y=0.5 ทำให้ถือพอร์ตเสี่ยงครึ่งหนึ่งและ risk-free อีกครึ่ง

ตัวอย่างพอร์ตเสี่ยงที่ mean 10% และ SD 15% ต่อปี ผสมครึ่งหนึ่งกับ risk-free 2% จะให้ mean 2%+0.5(10%−2%)=6%2\%+0.5(10\%-2\%)=6\% และ SD 0.5(15%)=7.5%0.5(15\%)=7.5\% ต่อปี การลดความผันผวนในสูตรนี้มาพร้อมกับลด expected return เข้าหา 2%

Sharpe ratio คือความชันจากจุด risk-free

เมื่อกำหนดสินทรัพย์ปลอดความเสี่ยงแล้ว สิ่งที่ผู้ลงทุนคาดว่าจะได้รับเพิ่มจากรับความเสี่ยงคือ expected excess return μP−rf\mu_P-r_f เราหารส่วนเพิ่มนี้ด้วย SD เพื่อได้ Sharpe ratio ของแบบจำลองหนึ่งช่วงเวลา:

SR⁡P=μP−rfσP.\operatorname{SR}_P=\frac{\mu_P-r_f}{\sigma_P}.

ในกราฟที่แกนนอนเป็น SD และแกนตั้งเป็น expected return จุด risk-free อยู่ที่ (0,rf)(0,r_f) ความชันของเส้นจากจุดนั้นไปยังพอร์ตเสี่ยงคือ Sharpe ratio พอดี พอร์ตที่ได้ผลตอบแทนสูงกว่าอาจไม่ได้มี Sharpe สูงกว่า หากต้องเพิ่ม SD มากกว่าส่วนผลตอบแทนที่เพิ่มขึ้น William F. Sharpe: The Sharpe Ratio

ถ้าจะประมาณ Sharpe จากข้อมูลรายเดือน ให้คำนวณ excess return รายเดือนแล้วใช้ค่าเฉลี่ยเลขคณิตหารด้วย SD ของ excess return ในความถี่เดียวกัน เมื่อ risk-free คงที่ SD ของ excess return เท่ากับ SD ของผลตอบแทนพอร์ต การคูณ Sharpe รายเดือนด้วย 12\sqrt{12} อาศัยเงื่อนไขการแปลงเวลาที่เหมาะสม โดยเฉพาะความสัมพันธ์ข้ามเวลา ส่วน CAGR หาร SD เป็นตัววัดอีก convention หนึ่งและไม่ตรงกับนิยาม arithmetic Sharpe นี้เสมอไป

ใน Lab บางช่วงใช้ annualized compounded return เป็นตัวเลขผลตอบแทนบนกราฟ เราใช้ expected arithmetic return ต่อปี ที่สมมติขึ้นในหน้านี้ เพื่อให้ wTμw^\mathsf{T}\mu, 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 เดิมยิ่งสูง สูตรเป้าหมายคือ

minimize⁡w[−wTμ−rfwTΣw],∑iwi=1,0≤wi≤1.\underset{w}{\operatorname{minimize}} \left[-\frac{w^\mathsf{T}\mu-r_f}{\sqrt{w^\mathsf{T}\Sigma w}}\right], \qquad\sum_iw_i=1,\quad0\leq w_i\leq1.

เริ่มจากฟังก์ชันที่รับ 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 แทน yy ในสูตรต้นบท 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 การแสดงแถวนี้ต้องสมมติว่ากู้และให้กู้ได้ที่ rfr_f เดียวกัน รวมถึงไม่มีต้นทุนหรือวงเงินกู้ที่ขัดกับน้ำหนัก หากกู้แพงกว่าอัตราที่รับจากเงินปลอดความเสี่ยง เส้นช่วงลงทุนเกิน 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 ร่วมกันได้ ภายใต้สมมติฐานว่ากู้และให้กู้ที่ rfr_f เดียวกัน และไม่มีเพดานกู้ที่ขัดกับส่วนผสมที่ต้องการ หาก 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 ซึ่งแบ่งเงินเท่ากันทุกสินทรัพย์ สำหรับสามตัวใช้น้ำหนัก 1/31/3 ต่อชื่อ 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 ประมาณ σ/n\sigma/\sqrt n เช่น SD 20% ต่อปีและข้อมูลผลตอบแทนรายปีอิสระ 25 ปี ให้ standard error ของ mean ประมาณ 20%/25=4%20\%/\sqrt{25}=4\% ต่อปี การมีค่าเฉลี่ยย้อนหลังจึงไม่ได้แปลว่า 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 ผิด การกระจายความเสี่ยงที่เห็นในแบบจำลองอาจไม่เกิดตามนั้น

สำหรับ NN สินทรัพย์ covariance มีตัวเลขไม่ซ้ำ N(N+1)/2N(N+1)/2 ค่า เมื่อมีจำนวนเดือนน้อยเมื่อเทียบกับจำนวนสินทรัพย์ การประมาณหลายค่าพร้อมกันทำได้ยากขึ้น ถ้าตัวอย่างมี TT แถว sample covariance ที่หัก mean แล้วมี rank ไม่เกิน T−1T-1 ดังนั้นกรณี N≥TN\geq T จะไม่สามารถเป็น 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 เดือนที่แปดคือ (1+RA,7)(1+RA,8)(1+R_{A,7})(1+R_{A,8}) จากนั้นถ่วงด้วยน้ำหนักที่เลือกไว้เมื่อเริ่มช่วงทดสอบแล้วรวมมูลค่า วิธีนี้คิดการถือสินทรัพย์เดิม โดยไม่บังคับน้ำหนักกลับสู่เป้าทุกเดือน

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 (0.14−0.02)/0.20=0.6(0.14-0.02)/0.20=0.6 ส่วน MSR สามสินทรัพย์มี mean ต่ำกว่าที่ประมาณ 9.7290% แต่ SD 10.1879% และ Sharpe ประมาณ 0.758640 จึงให้ส่วนเพิ่มเหนือ risk-free ต่อหนึ่งหน่วย SD สูงกว่าในแบบจำลองนี้

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

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

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

ปิด
คำค้นหา

© 2023 by QuantCorner, Thailand.

bottom of page