Machine Learning · ภาวะตลาด · บทย่อย 3
Endowment Simulation: เงินทุน การใช้จ่าย และลำดับผลตอบแทน
กองทุนที่จ่ายทุนการศึกษาทุกปีต้องดูทั้งเงินที่เหลือปลายทางและความสามารถจ่ายระหว่างทาง แม้ผลตอบแทนสองเส้นทางมีค่าเฉลี่ยเท่ากัน การขาดทุนก่อนหรือหลังถอนเงินอาจทำให้เงินเหลือต่างกันได้
Endowment คือเงินทุนที่นำไปลงทุนเพื่อสนับสนุนการใช้จ่ายต่อเนื่อง เช่น เงินทุนของมหาวิทยาลัย ตัวอย่างนี้เริ่มจากเงินสมมติ 100 ล้านบาทในกำลังซื้อปีฐาน คงสัดส่วนลงทุนต้นปี A 60% และ B 40% แล้วจ่ายค่าใช้จ่ายจริงปีละ 2.5 ล้านบาท ณ สิ้นปี โดยเพิ่มจำนวนเงินตามเงินเฟ้อ
เราจะเปรียบเทียบสองแบบจำลองที่มีการแจกแจงสถานะของแต่ละปีเหมือนกัน แต่มีความต่อเนื่องของสถานะต่างกัน พอร์ตทั้งคู่ใช้กฎการลงทุนและการจ่ายเงินเดียวกัน การทดลองจึงวัดผลของลำดับสถานการณ์ภายใต้ input ที่กำหนด ไม่ได้เป็นการปรับพอร์ตให้รู้ภาวะตลาดล่วงหน้า
โค้ดเป็นตัวอย่างใหม่ ใช้ NumPy และ pandas และรันจาก Notebook ว่างได้ ไม่ต้องมีราคาหุ้นหรือ Notebook ของผู้สอน เงินในโค้ดมีหน่วยล้านบาท ส่วนผลตอบแทนและเงินเฟ้อเก็บเป็นทศนิยมต่อปี
import numpy as np
import pandas as pd
np.set_printoptions(precision=6, suppress=True)
ลองสลับสองปีก่อนสร้างหลายพันเส้นทาง
เริ่มด้วยตัวอย่างย่อยที่ไม่มีเงินเฟ้อ: เงินต้น 100 ล้านบาท ใช้จ่ายปลายปีปีละ 5 ล้านบาท และมีผลตอบแทนสองปีคือ +20% กับ −10% กรณีแรกได้กำไรก่อน:
กรณีที่สองขาดทุนก่อน:
หากไม่ถอนเงิน ทั้งสองลำดับจบที่ เท่ากัน แต่เมื่อถอนเงิน จำนวนเงินที่ได้รับผลตอบแทนปีถัดไปต่างกัน จึงจบต่างกัน 1.5 ล้านบาท
def spend_after_return(real_returns, initial=100.0, spending=5.0):
wealth = [float(initial)]
for annual_return in real_returns:
wealth.append(max(wealth[-1] * (1 + annual_return) - spending, 0.0))
return np.array(wealth)
sequence_early_gain = spend_after_return([0.20, -0.10])
sequence_early_loss = spend_after_return([-0.10, 0.20])
print("Gain first:", sequence_early_gain)
print("Loss first:", sequence_early_loss)
print("Without spending:", 100 * 1.2 * 0.9)
wealth เริ่มด้วยเงินต้นก่อนปีแรก แล้วเพิ่มเงินคงเหลือปลายแต่ละปี max(..., 0) จำกัดเงินที่เหลือไม่ให้ติดลบ ฟังก์ชันสั้นนี้ใช้สอนลำดับเหตุการณ์ ส่วนฟังก์ชันเต็มข้างล่างจะบันทึกด้วยว่าจ่ายเงินไม่ครบเท่าไรเมื่อทุนไม่พอ
ความเสี่ยงจากลำดับผลตอบแทนหรือ sequence risk จึงเกี่ยวข้องกับ cash flows ขนาดและเวลาของการถอนเงิน กรณีนี้ไม่ได้เกิดจาก mean หรือ volatility ของสองผลตอบแทนต่างกัน เพราะใช้ตัวเลขชุดเดียวกันแล้วสลับลำดับเท่านั้น
ตัวอย่างหลักต่อไปใช้จ่าย 2.5 ล้านบาทจริงต่อปี ไม่ใช่ 5 ล้านบาทของตัวอย่างสองปี และใส่เงินเฟ้อ 2% เพื่อให้เห็นบัญชีเงินทั้ง nominal และ real
ระบุหนึ่งปีก่อนเขียนลูป
กฎของตัวอย่างหลักมีดังนี้:
| รายการ | สมมติฐาน |
|---|---|
| เงินเริ่มต้น | 100 ล้านบาท ณ กำลังซื้อปีฐาน |
| ระยะเวลา | 30 ปี |
| น้ำหนักต้นปี | A 60%, B 40%, ไม่มี short หรือ leverage |
| การปรับพอร์ต | กลับสู่น้ำหนักเป้าหมายต้นทุกปี และถือสินทรัพย์โดยไม่ปรับระหว่างปี |
| เงินที่ต้องจ่าย | 2.5 ล้านบาทต่อปีในกำลังซื้อปีฐาน จ่ายหลังรับผลตอบแทนสิ้นปี |
| เงินเฟ้อ | 2% ต่อปี คงที่ |
| เมื่อเงินไม่พอ | จ่ายเท่าที่มี บันทึกส่วนที่จ่ายไม่ได้ ทุนคงเหลือเป็นศูนย์ |
| ต้นทุนและเงินเข้าระหว่างทาง | ไม่มีค่าซื้อขาย ภาษี เงินบริจาค หรือเงินเติม |
| จำนวนเส้นทาง | 20,000 เส้นทาง ซึ่งสุ่มอย่างอิสระจากกัน |
กำหนดสถานะ Calm/Stress และพารามิเตอร์รายปีเหมือน Regime Scenarios คือ Calm มี mean ของ A/B 8%/3%, Stress มี −12%/2% และ covariance ตามตารางในบทก่อนหน้า เราจะสร้างตัวแปรเหล่านี้ใหม่เพื่อให้รันบทนี้ลำพังได้
endowment_transition = np.array([[0.85, 0.15], [0.35, 0.65]])
endowment_stationary = np.array([0.70, 0.30])
endowment_means = np.array([[0.08, 0.03], [-0.12, 0.02]])
endowment_covs = np.array([[[0.0100, 0.0010], [0.0010, 0.0025]],
[[0.0400, -0.0020], [-0.0020, 0.0064]]])
endowment_weights = np.array([0.60, 0.40])
endowment_initial = 100.0
endowment_spending = 2.5
endowment_inflation = 0.02
endowment_years = 30
endowment_n = 20000
print("Annual nominal means by state:", endowment_means @ endowment_weights)
print("Year 1 required nominal spending:", endowment_spending * (1 + endowment_inflation))
endowment_spending = 2.5 คือจำนวนเงินจริงคงที่ ไม่ใช่ถอน 2.5% ของเงินคงเหลือทุกปี เงินที่ต้องจ่าย nominal ในสิ้นปีแรกเท่ากับ ล้านบาท ส่วนสิ้นปีที่สองเท่ากับ ล้านบาท
กฎถอนเป็นเปอร์เซ็นต์ของทุน กฎเฉลี่ยทุนย้อนหลัง หรือกฎลดรายจ่ายเมื่อทุนต่ำกว่าเกณฑ์จะให้ผลอีกแบบหนึ่ง ต้องเขียน recurrence ใหม่ให้ตรงนโยบายก่อนเปรียบเทียบ การจำลองนี้ไม่เลื่อนยอดค้างจ่ายไปเป็นหนี้ในปีต่อไป จึงรายงาน missed spending เพื่อแยกความเสียหายส่วนนั้นจากเงินคงเหลือ
สถานะรายปีเหมือนกัน แต่การอยู่ต่อไม่เหมือนกัน
แบบ Markov ใช้
แบบ IID สุ่มสถานะทุกปีใหม่อย่างอิสระด้วยโอกาส Calm 70% และ Stress 30% จึงเขียน transition ได้ว่า
เมื่อเริ่มทั้งสองแบบด้วย ความน่าจะเป็น Stress ของทุกปีเป็น 30% เท่ากัน แต่ Markov มีโอกาส Stress ต่อหลังปี Stress 65% ขณะที่ IID มีเพียง 30% ช่วงที่สถานะเดียวกันเกิดติดกันจึงมีโครงสร้างต่างกัน
def state_paths(uniforms, transition, initial_probabilities):
n, years = uniforms.shape
states = np.empty((n, years), dtype=int)
states[:, 0] = uniforms[:, 0] >= initial_probabilities[0]
for t in range(1, years):
states[:, t] = uniforms[:, t] >= transition[states[:, t - 1], 0]
return states
endowment_rng = np.random.default_rng(402)
endowment_uniforms = endowment_rng.random((endowment_n, endowment_years))
endowment_shocks = endowment_rng.uniform(-np.sqrt(3), np.sqrt(3),
size=(endowment_n, endowment_years, 2))
endowment_states = state_paths(endowment_uniforms, endowment_transition, endowment_stationary)
endowment_iid_transition = np.tile(endowment_stationary, (2, 1))
endowment_iid_states = state_paths(endowment_uniforms, endowment_iid_transition, endowment_stationary)
print("First Markov path:", endowment_states[0, :10])
print("First IID path:", endowment_iid_states[0, :10])
print("Same first-year states:", np.array_equal(endowment_states[:, 0], endowment_iid_states[:, 0]))
state_paths เป็น helper สำหรับสองสถานะโดยเฉพาะ ทุกแถวคือหนึ่งเส้นทาง ทุกคอลัมน์คือหนึ่งปี ตั้งปีแรกด้วย initial probabilities แล้วใช้สถานะปีก่อนเลือกแถวของ transition สำหรับปีต่อไป uniforms[:, t] เลือกเลขสุ่มของทุกเส้นทางในปีที่ พร้อมกัน
เราใช้เลขสุ่ม endowment_uniforms ชุดเดียวกันกับทั้งสองโมเดล และใช้ shocks ชุดเดียวกันด้วย เรียกว่า common random numbers เพื่อเปรียบเทียบโดยจับคู่เส้นทาง ไม่ได้ทำให้สถานะหลังปีแรกต้องเหมือนกัน เพราะ threshold ที่ใช้เลือกสถานะต่างกัน
ปีแรกใช้ prior เดียวกันและเลขสุ่มเดียวกัน จึงได้สถานะตรงกันทุกเส้นทาง ผล Same first-year states เป็น True ส่วนตัวอย่างสิบปีแรกของเส้นทางแรกเริ่มต่างกันภายหลัง โค้ดใช้ seed 402 คงที่ ไม่เปลี่ยน seed เพื่อคัดผลการเปรียบเทียบ
สร้างผลตอบแทนที่มี mean และ covariance ตามสถานะ
การจำลองเงินคงเหลือต้องควบคุม gross return ให้มีความหมายสำหรับสินทรัพย์ที่ไม่ใช้ leverage ตัวอย่างนี้จึงใช้ shocks แบบ Uniform มีขอบเขต แทน Normal ของ simple return ที่มีหางต่ำกว่า −100% ทางทฤษฎี
ให้สมาชิกของเวกเตอร์ เป็นอิสระและแจกแจง Uniform บน จะมี mean 0 และ variance 1 เมื่อ ให้
จะได้ conditional mean และ covariance ตามเดิม แต่รูปร่างการแจกแจงมีขอบเขตและไม่ใช่ Normal การมี moments เท่ากันไม่ได้ทำให้ tail risk ของสองการแจกแจงเท่ากัน
def asset_returns_by_state(states, shocks, means, covariances):
returns = np.empty_like(shocks)
for state in range(len(means)):
mask = states == state
L = np.linalg.cholesky(covariances[state])
returns[mask] = means[state] + shocks[mask] @ L.T
return returns
endowment_returns = asset_returns_by_state(endowment_states, endowment_shocks, endowment_means, endowment_covs)
endowment_iid_returns = asset_returns_by_state(endowment_iid_states, endowment_shocks, endowment_means, endowment_covs)
print("Return array shape:", endowment_returns.shape)
print(f"Smallest simulated asset return: {100 * endowment_returns.min():.4f}%")
print("Realized stress share, Markov:", round(endowment_states.mean(), 6))
print("Realized stress share, IID:", round(endowment_iid_states.mean(), 6))
mask เลือกทุกตำแหน่งที่อยู่ในสถานะนั้น shocks[mask] ได้ตารางแถวละสองสินทรัพย์ แล้วคูณ L.T ตาม convention ที่แต่ละ observation เป็นแถว ผลลัพธ์มี shape (20000, 30, 2) คือเส้นทาง ปี และสินทรัพย์ตามลำดับ
สำหรับ A ใน Stress ขอบล่างตามโมเดลคือ ซึ่งยังสูงกว่า −100% ส่วน B และ Calm ก็มี gross return เป็นบวกภายใต้ค่าที่กำหนดนี้ การตรวจนี้มาจากขอบเขตของ shocks และพารามิเตอร์ ไม่ใช่สรุปจากการที่ยังไม่เคยสุ่มเจอผลตอบแทนต่ำกว่า −100% เท่านั้น
สัดส่วนปี Stress ที่สุ่มได้ทั้งหมดประมาณ 30.0955% ใน Markov และ 30.1195% ใน IID ไม่ต้องตรง 30% พอดี จำนวนปีในเส้นทาง Markov ไม่เป็นอิสระกัน แต่เส้นทางแต่ละแถวเป็นการทดลองอิสระ ซึ่งเราจะใช้เป็นหน่วยนับสำหรับ MC standard error
เขียนบัญชีผลตอบแทนก่อนจ่ายเงิน
ให้ เป็นเงิน nominal หลังจ่ายสิ้นปี , เป็นดัชนีราคาเมื่อเงินเฟ้อ และ เป็นรายจ่ายจริงที่ต้องการในแต่ละปี ขั้นตอนหนึ่งปีคือ
คือเงินหลังรับผลตอบแทนแต่ก่อนจ่าย คือยอดที่ต้องจ่ายตามราคาในปีนั้น หารเงินทุกจำนวนของปลายปี ด้วย จึงได้กำลังซื้อปีฐาน เราจะเก็บ wealth_real, paid_real และ unpaid_real แยกกัน
def simulate_endowment(asset_returns, weights, initial, spending, inflation):
r, w = np.asarray(asset_returns, float), np.asarray(weights, float)
if (r.ndim != 3 or w.shape != (r.shape[2],) or not np.isfinite(r).all()
or not np.isfinite(w).all() or np.any(r <= -1) or np.any(w < 0)
or not np.isclose(w.sum(), 1) or not np.isfinite([initial, spending, inflation]).all()
or initial <= 0 or spending < 0 or inflation <= -1):
raise ValueError("Check positive wealth, long-only weights, return array and cash-flow assumptions")
portfolio_returns = r @ w
n, years = portfolio_returns.shape
price_index = (1 + inflation) ** np.arange(years + 1)
nominal = np.full(n, initial)
wealth_real = np.empty((n, years + 1))
wealth_real[:, 0] = initial
paid_real = np.empty((n, years))
unpaid_real = np.empty((n, years))
for t in range(years):
available = nominal * (1 + portfolio_returns[:, t])
required = spending * price_index[t + 1]
paid = np.minimum(available, required)
nominal = available - paid
wealth_real[:, t + 1] = nominal / price_index[t + 1]
paid_real[:, t] = paid / price_index[t + 1]
unpaid_real[:, t] = (required - paid) / price_index[t + 1]
return {"wealth_real": wealth_real, "paid_real": paid_real,
"unpaid_real": unpaid_real, "portfolio_returns": portfolio_returns}
ฟังก์ชันรับ returns สามมิติและน้ำหนักหนึ่งมิติ ตรวจค่า finite, gross return บวก, น้ำหนักไม่ติดลบและรวมหนึ่ง จากนั้น r @ w ยุบแกนสินทรัพย์ให้เหลือ (จำนวนเส้นทาง, จำนวนปี) การถ่วงด้วยน้ำหนักเดิมทุกปีหมายถึงกลับสู่น้ำหนักนั้นต้นปี ไม่ใช่ buy-and-hold น้ำหนักที่ลอยต่อเนื่องตลอด 30 ปี
wealth_real มี 31 คอลัมน์ รวมเงินต้นที่เวลา 0 ส่วนเงินจ่ายมี 30 คอลัมน์ตาม 30 สิ้นปี np.minimum จำกัดการจ่ายไม่เกินเงินที่มี ถ้าทุนเหลือศูนย์และไม่มีเงินเติม ทุนจะเป็นศูนย์ต่อไป และรายจ่ายปีต่อไปจะถูกบันทึกเป็น unpaid
ยอดถอนเป็น cash flow แยกจากผลตอบแทนลงทุน อย่านำ (wealth_after / wealth_before) - 1 ไปเรียกผลตอบแทนกองทุนโดยไม่ปรับเงินจ่าย เช่น พอร์ตได้กำไรแต่ถอนเงินมาก มูลค่ากองทุนก็อาจลดลงได้ การวัด drawdown ของมูลค่าหลังจ่ายเงินจึงไม่ใช่ investment drawdown ของดัชนีที่ไม่มี cash flow
ตรวจเส้นทางเดียวให้ตรงก่อนรันทั้งหมด
ผลตอบแทนจริงเมื่อ nominal return เป็น และเงินเฟ้อเป็น เท่ากับ
การลบ เป็นเพียงค่าประมาณ ไม่ใช่สูตร exact เมื่อกลับไปใช้เงินจริง recurrence จึงเขียนได้ว่า
endowment_demo = simulate_endowment(endowment_returns[:1, :3], endowment_weights,
endowment_initial, endowment_spending, endowment_inflation)
print(pd.DataFrame({"Nominal return (%)": 100 * endowment_demo["portfolio_returns"][0],
"Real spending paid": endowment_demo["paid_real"][0],
"End-year real wealth": endowment_demo["wealth_real"][0, 1:]}, index=[1, 2, 3]).round(4))
endowment_first_return = endowment_demo["portfolio_returns"][0, 0]
endowment_first_real = (1 + endowment_first_return) / (1 + endowment_inflation) - 1
print(f"First-year real return: {100 * endowment_first_real:.4f}%")
print("First-year real wealth check:", round(100 * (1 + endowment_first_real) - 2.5, 6))
เส้นทางแรกได้ nominal return ปีแรกประมาณ 7.6849% คิดเป็น real return 5.5735% จึงมีเงินจริงหลังจ่ายประมาณ ล้านบาท อีกสองปีถัดไปเหลือประมาณ 106.9174 และ 104.2938 ล้านบาทจริง
แม้ปีที่สองมีสถานะ Stress ในการสุ่ม ผลตอบแทนพอร์ตปีนั้นยังบวกได้ สถานะกำหนดการแจกแจงของผลตอบแทน ไม่ได้บังคับเครื่องหมายทุก observation และกฎพอร์ต 60/40 นี้ไม่ใช้ป้ายสถานะมาทำนายหรือตัดสินใจเปลี่ยนน้ำหนัก
แยกเงินปลายทางไม่ถึงเป้าจากการจ่ายไม่ครบ
กำหนดเป้าทุนปลายปี 30 เป็น 50 ล้านบาทในกำลังซื้อปีฐาน เราจะนับสองเหตุการณ์แยกกัน:
- Terminal shortfall: เงินจริงปลายปี 30 ต่ำกว่า 50 ล้านบาท
- Any unpaid spending: มีอย่างน้อยหนึ่งปีที่จ่ายเงินจริง 2.5 ล้านบาทไม่ครบ
กองทุนอาจจ่ายครบทุกปีแต่เหลือทุนเพียง 40 ล้านบาท จึงเกิดเหตุการณ์แรกโดยไม่เกิดเหตุการณ์ที่สอง ส่วนในแบบจำลองที่ไม่มีเงินเติมนี้ การจ่ายไม่ครบทำให้เงินหลังจ่ายเป็นศูนย์ และจึงต่ำกว่าเป้าปลายทางบวกด้วย
endowment_markov = simulate_endowment(endowment_returns, endowment_weights,
endowment_initial, endowment_spending, endowment_inflation)
endowment_iid = simulate_endowment(endowment_iid_returns, endowment_weights,
endowment_initial, endowment_spending, endowment_inflation)
endowment_goal = 50.0
def endowment_statistics(result, goal):
terminal = result["wealth_real"][:, -1]
shortfall = terminal < goal
missed_payment = np.any(result["unpaid_real"] > 1e-10, axis=1)
return {"Mean terminal wealth": terminal.mean(), "Median terminal wealth": np.median(terminal),
"5th percentile terminal wealth": np.quantile(terminal, 0.05),
"Terminal shortfall probability": shortfall.mean(),
"Any unpaid spending probability": missed_payment.mean()}
endowment_summary = pd.DataFrame({"Markov": endowment_statistics(endowment_markov, endowment_goal),
"IID": endowment_statistics(endowment_iid, endowment_goal)}).T
print(endowment_summary.round(6).to_string())
terminal < goal สร้าง Boolean หนึ่งค่าต่อเส้นทาง ค่า True ถูกนับเป็นหนึ่งเมื่อหา mean จึงได้สัดส่วนเส้นทางที่พลาดเป้าหมาย ส่วน any(..., axis=1) ตรวจว่ามี unpaid อย่างน้อยหนึ่งปีในแต่ละเส้นทางก่อนคำนวณ mean ต้องไม่เฉลี่ยช่อง unpaid ทุกปีแล้วเรียกว่าโอกาสที่เส้นทางหนึ่งเคยจ่ายไม่ครบ
| ผลจาก 20,000 เส้นทาง | Markov | IID |
|---|---|---|
| Mean เงินจริงปลายทาง | 40.7139 | 33.1662 |
| Median เงินจริงปลายทาง | 20.2575 | 19.3570 |
| Percentile 5 ของเงินจริงปลายทาง | 0 | 0 |
| โอกาสเงินปลายทางต่ำกว่า 50 | 70.315% | 75.635% |
| โอกาสเคยจ่ายไม่ครบ | 29.550% | 26.165% |
Mean สูงกว่า median มากเพราะบางเส้นทางจบด้วยทุนสูง ขณะที่หลายเส้นทางเหลือทุนต่ำหรือศูนย์ การดู mean อย่างเดียวจึงไม่เห็นรูปร่างของผลลัพธ์ สำหรับ input ชุดนี้ Markov มีโอกาสพลาดเป้าปลายทางต่ำกว่า แต่มีโอกาสเคยจ่ายไม่ครบสูงกว่า IID การจัดอันดับความเสี่ยงจึงเปลี่ยนตามเหตุการณ์ที่ถาม
สองโมเดลมี one-year marginal distribution เดียวกันเมื่อเริ่ม stationary แต่ผลตอบแทนข้ามปีใน Markov มี dependence ผ่านสถานะ ลำดับปีที่ดีหรือไม่ดีติดกันส่งผลต่อการทบต้นและเงินที่เหลือหลังใช้จ่าย ผลนี้ไม่ใช่ข้อสรุปว่าโมเดล Markov ให้ผลดีกว่าหรือแม่นกว่าตลาดจริงโดยทั่วไป
วัดความคลาดเคลื่อนจากจำนวนเส้นทาง
ถ้าสุ่ม เส้นทางเป็นอิสระ และนับได้สัดส่วน shortfall ค่า MC standard error ของสัดส่วนประมาณด้วย
ส่วน mean เงินปลายทางใช้ sample SD ของเงินปลายทางหาร ไม่ใช้ volatility ของผลตอบแทนรายปีแทน เพราะเป็นสถิติคนละตัว
endowment_shortfall = endowment_markov["wealth_real"][:, -1] < endowment_goal
endowment_probability = endowment_shortfall.mean()
endowment_probability_se = np.sqrt(endowment_probability * (1 - endowment_probability) / endowment_n)
endowment_terminal = endowment_markov["wealth_real"][:, -1]
endowment_terminal_se = endowment_terminal.std(ddof=1) / np.sqrt(endowment_n)
print(f"Shortfall probability: {100 * endowment_probability:.4f}%")
print(f"Probability MC standard error: {100 * endowment_probability_se:.4f} percentage points")
print(f"Mean terminal wealth MC standard error: {endowment_terminal_se:.6f}")
สำหรับ Markov ได้ probability 70.315% และ MC SE ประมาณ 0.3231 จุดเปอร์เซ็นต์ ส่วน mean เงินปลายทางมี SE ประมาณ 0.387201 ล้านบาทจริง ตัวเลขนี้วัดความคลาดเคลื่อนจากการสุ่ม finite paths ภายใต้โมเดลเดิม ไม่รวมความคลาดเคลื่อนของ transition, means, covariance, เงินเฟ้อ หรือนโยบายจ่ายเงิน
หากจำลองแล้วไม่พบ shortfall เลย สูตร plug-in ข้างต้นจะให้ SE ศูนย์ ซึ่งไม่ได้พิสูจน์ว่าเหตุการณ์เป็นไปไม่ได้ ต้องรายงานจำนวนเส้นทางและใช้ขอบเขตความน่าจะเป็นที่เหมาะสม เช่น ขอบบนแบบ binomial เมื่อพบศูนย์เหตุการณ์ แทนการสรุปความเสี่ยงศูนย์
จำนวนปี 30 ไม่ใช่จำนวนตัวอย่างอิสระเพิ่มอีก 30 เท่าสำหรับ terminal shortfall หนึ่งเส้นทางให้คำตอบเหตุการณ์ปลายทางเพียงหนึ่งค่า จึงใช้ เป็นตัวหารของ SE
เปรียบเทียบสองโมเดลโดยรักษาคู่การสุ่ม
เนื่องจากใช้ random numbers ร่วมกัน ผลของ Markov และ IID ในเส้นทางหมายเลขเดียวกันจึงสัมพันธ์กัน หากต้องการ SE ของความต่าง probability ให้สร้าง
เป็น −1, 0 หรือ 1 แล้วคำนวณ mean และ sample SD ของ โดยตรง
endowment_iid_shortfall = endowment_iid["wealth_real"][:, -1] < endowment_goal
endowment_paired_difference = endowment_shortfall.astype(float) - endowment_iid_shortfall.astype(float)
endowment_difference_se = endowment_paired_difference.std(ddof=1) / np.sqrt(endowment_n)
print(f"Markov-minus-IID shortfall probability: {100 * endowment_paired_difference.mean():.4f} percentage points")
print(f"Paired-difference MC standard error: {100 * endowment_difference_se:.4f} percentage points")
ความต่าง Markov ลบ IID ได้ −5.3200 จุดเปอร์เซ็นต์ และ paired MC SE ประมาณ 0.2973 จุดเปอร์เซ็นต์ การรวม SE สองฝั่งแบบสมมติอิสระจะไม่ใช้ covariance ของการจับคู่ที่เราสร้างไว้
การจับคู่เส้นทางช่วยประเมินความต่างของโมเดลภายใต้ random inputs ร่วมกัน ไม่ใช่หลักฐานว่าทั้งสองโมเดลต้องให้ลำดับผลตอบแทนจริงแบบใด การทดสอบกับข้อมูลตลาดยังต้องประเมิน conditional distributions และ transition จากข้อมูลในอดีตที่กันช่วงตรวจไว้
เปลี่ยนรายจ่ายบนเส้นทางเดิม
คราวนี้คงผลตอบแทน Markov ทั้ง 20,000 เส้นทางไว้ แล้วเปลี่ยนรายจ่ายจริงเป็น 2.0, 2.5 และ 3.0 ล้านบาทต่อปี การใช้เส้นทางเดิมทำให้เปรียบเทียบผลของรายจ่ายโดยไม่ปนกับการสุ่มชุดใหม่
endowment_sensitivity = []
for real_spending in [2.0, 2.5, 3.0]:
result = simulate_endowment(endowment_returns, endowment_weights,
endowment_initial, real_spending, endowment_inflation)
row = {"Annual real spending": real_spending}
row.update(endowment_statistics(result, endowment_goal))
endowment_sensitivity.append(row)
endowment_sensitivity = pd.DataFrame(endowment_sensitivity)
print(endowment_sensitivity.round(6).to_string(index=False))
เมื่อเพิ่มรายจ่ายจาก 2.0 เป็น 3.0 ล้านบาท โอกาสพลาดเป้าปลายทางเพิ่มจาก 60.580% เป็น 78.585% และโอกาสเคยจ่ายไม่ครบเพิ่มจาก 17.100% เป็น 43.675% ภายใต้ผลตอบแทนและสมมติฐานชุดเดิม ในแต่ละเส้นทาง รายจ่ายที่มากขึ้นไม่ทำให้เงินคงเหลือมากขึ้น จึงตรวจความสอดคล้องนี้ได้ด้วยการเทียบรายเส้นทางด้วย
นี่เป็น sensitivity analysis สามค่าที่ตั้งไว้ ยังไม่ได้หานโยบายรายจ่ายที่เหมาะสม สำหรับนโยบายที่เปลี่ยนรายจ่ายตามทุน ต้องระบุว่าใช้ทุนก่อนหรือหลังผลตอบแทน ใช้ข้อมูลถึงเมื่อไร มี minimum spending หรือไม่ และเมื่อจ่ายไม่ครบยอดนั้นเป็นภาระผูกพันต่อหรือยกเลิกไป
หากจะเพิ่ม regime-aware allocation ต้องใช้ probability ที่คำนวณจากข้อมูลที่รู้ก่อนลงทุน รวมต้นทุนเมื่อเปลี่ยนน้ำหนัก และเทียบกับ fixed mix ภายใต้ cash flows เดียวกัน การใช้ hidden state ที่โปรแกรมรู้ตอนสร้างข้อมูลไปจัดพอร์ตโดยตรงจะทำให้นักลงทุนจำลองมีข้อมูลเหนือกว่าที่นักลงทุนจริงเห็น
แบบฝึกหัดพร้อมเฉลย
- เงินต้น 100 ผลตอบแทนสองปี +20% กับ −10% และถอนปลายปีปีละ 5 เหตุใดกำไรก่อนจึงจบสูงกว่า 1.5
เฉลยข้อ 1
กรณีกำไรก่อน เงิน 5 ที่ถอนปีแรกไม่ต้องรับผลตอบแทน −10% ในปีที่สอง ส่วนกรณีขาดทุนก่อน เงิน 5 ที่ถอนปีแรกพลาดผลตอบแทน +20% ของปีที่สอง ความต่างจึงเป็น ภายใต้ลำดับและ cash flow ที่กำหนด
- รายจ่ายจริง 2.5 และเงินเฟ้อ 2% ต้องใช้เงิน nominal สิ้นปีที่สองเท่าไร
เฉลยข้อ 2
ล้านบาท หากยังจ่าย nominal 2.5 เท่าเดิม กำลังซื้อของการจ่ายจะลดลง จึงเป็นคนละนโยบายกับรายจ่ายจริงคงที่
- พอร์ตได้ nominal return 5% เงินเฟ้อ 2% เริ่มด้วยเงินจริง 100 และจ่ายเงินจริง 2.5 ปลายปี เหลือเงินจริงเท่าไร
เฉลยข้อ 3
ล้านบาท real return เท่ากับประมาณ 2.941176% ไม่ใช่ 3% พอดี
- เหตุใดสถานะ Stress 30% ทุกปีจึงยังไม่ทำให้ Markov กับ IID ให้ terminal wealth distribution เดียวกัน
เฉลยข้อ 4
Marginal probability ของปีเดียวกันไม่กำหนด joint distribution ข้ามเวลา Markov ของตัวอย่างมีโอกาสอยู่ Stress ต่อ 65% ส่วน IID 30% ลำดับที่ติดกันต่างกันจึงเปลี่ยนผลการทบต้นและ cash flow แม้การแจกแจงรายปีเหมือนกัน
- ถ้า 20,000 เส้นทางมี 1,000 เส้นทางที่เคยจ่ายไม่ครบ โดยแต่ละเส้นทางขาดเฉลี่ยสี่ปี โอกาสเคยจ่ายไม่ครบเป็นเท่าไร
เฉลยข้อ 5
เท่ากับ ต้องนับเส้นทางที่เกิดอย่างน้อยหนึ่งครั้ง ไม่ใช่นำจำนวนปีที่ขาดทั้งหมด 4,000 หาร 20,000 แล้วตอบ 20% และไม่ใช่หารด้วยจำนวนช่อง path–year แล้วเรียกเป็นเหตุการณ์ระดับเส้นทาง
- ต้องการลด MC SE ของ probability จากประมาณ 0.32 เป็น 0.16 จุดเปอร์เซ็นต์ ควรเพิ่มจำนวนเส้นทางเป็นเท่าไรโดยประมาณ
เฉลยข้อ 6
เพิ่มสี่เท่าจาก 20,000 เป็น 80,000 เส้นทาง เมื่อโมเดลและ probability ใกล้เดิม เพราะ SE แปรตาม การเพิ่มจำนวนนี้ไม่ลดความผิดพลาดจากโมเดลที่ตั้ง transition หรือผลตอบแทนผิด
- ถ้าปีหนึ่งมีเงินหลังผลตอบแทนเท่ากับ 1 ล้านบาทจริง แต่ต้องจ่าย 2.5 ฟังก์ชันบันทึกอะไร
เฉลยข้อ 7
จ่ายได้ 1 บันทึก unpaid 1.5 และเงินหลังจ่ายเป็นศูนย์ ในแบบจำลองนี้ไม่มีการกู้หรือเงินเติม และไม่ยก unpaid ไปเป็นยอดหนี้ปีต่อไป หากนโยบายจริงถือว่ายังติดค้าง ต้องเพิ่มบัญชี liability แยก
- โปรแกรมรู้ว่าเส้นทางปีหน้าจะอยู่ Stress แล้วลดน้ำหนัก A ก่อนปีนั้น เป็น backtest ที่ใช้ข้อมูลได้จริงหรือไม่
เฉลยข้อ 8
ไม่ใช่ในแบบจำลอง hidden regime นี้ ป้ายสถานะเป็นข้อมูลของผู้สร้าง simulation นักลงทุนเห็น observations และต้องอนุมาน probabilities ที่รู้ทันเวลา หากต้องการวัดประโยชน์ของข้อมูลสมบูรณ์อาจทำเป็น oracle comparison ได้ แต่ต้องติดป้ายว่ามีข้อมูลที่นักลงทุนจริงไม่มี และไม่ปะปนกับกลยุทธ์ปฏิบัติได้
แหล่งที่มาและขอบเขตของตัวอย่าง
ทีมอ่าน transcript เต็มของ A multi regime model for a University Endowment และ NEW Lab session: regime-based investment model รวมทุกช่วงของ Lab เมื่อ 3 ตุลาคม 2026 พร้อมหน้า รายการอ้างอิงการวิเคราะห์ regimes
ไม่ได้ตรวจหรือรัน instructor Notebook โดยตรง และไม่แจกข้อมูลหรือโค้ดของผู้สอน การบรรยายกับ Lab ใช้ความถี่ เกณฑ์การใช้จ่าย และเกณฑ์ขาดทุนบางส่วนต่างกัน บทนี้จึงระบุตัวอย่างใหม่เป็นรายปี ใช้เงินเฟ้อคงที่ และจ่ายหลังผลตอบแทนอย่างสอดคล้องกันทุกขั้น
การใช้ simulation ประเมินหลายเส้นทางต่อจาก บท Monte Carlo และการแยกทรัพย์สินจากเป้าหมายต่อจาก Goal-based Allocation ผลที่รายงานเป็นผลของโมเดล bounded shocks, Markov/IID และนโยบายจ่ายเงินที่แสดงไว้ทั้งหมด ไม่ใช่ผลทดสอบกองทุนมหาวิทยาลัยแห่งใดหรือข้อเสนออัตราการใช้จ่ายจริง