Portfolio Management Toolkit Notes
Portfolio Management Toolkit Notes

Asset-Liability Management · บทย่อย 2

พันธบัตร กระแสเงินสด และ Duration

พันธบัตรจ่ายคูปอง 4% แต่ผู้ซื้อจ่ายต่ำกว่าเงินต้นครบกำหนด ผลตอบแทนของผู้ซื้อจะยังเป็น 4% หรือไม่?

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

เนื้อหาประกอบหัวข้อพันธบัตรและ Duration พร้อม Lab 126–127 ของ EDHEC/Coursera โค้ดแต่ละกรอบใช้ค่าจากกรอบก่อนหน้าในหน้านี้ แต่ไม่ต้อง import โค้ดจากบทอื่น ให้เริ่ม Notebook ใหม่ด้วยกรอบแรก

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

เริ่มจากเงินที่จะได้รับตามสัญญา

สมมติพันธบัตรมี face value หรือเงินต้นตามสัญญา 1,000 บาท อายุเหลือสามปี จ่ายคูปองปีละครั้งในอัตรา 4% ของ face value ดังนั้นได้รับคูปองปีละ 40 บาท และได้รับเงินต้น 1,000 บาทคืนพร้อมคูปองงวดสุดท้าย

เวลาจากวันนี้ คูปอง คืนเงินต้น เงินรับรวม
1 ปี 40 0 40
2 ปี 40 0 40
3 ปี 40 1,000 1,040

Coupon rate กำหนดเงินสดตามสัญญา ส่วนราคาที่ผู้ซื้อจ่ายวันนี้ขึ้นกับอัตราที่ตลาดต้องการและความเสี่ยงของตราสาร ถ้าคูปองคงที่ที่ 40 บาท ราคาซื้อ 900 บาทกับ 1,100 บาทย่อมให้ผลตอบแทนแก่ผู้ซื้อไม่เท่ากัน

import numpy as np
import pandas as pd

face_value = 1_000.0
coupon_rate = 0.04
cashflows = pd.Series([40.0, 40.0, 1_040.0], index=[1.0, 2.0, 3.0])
cashflows.index.name = "Years from today"
print(cashflows)
print(f"Total promised cash: {cashflows.sum():,.2f}")

เงินรับรวมตามสัญญาเท่ากับ 1,120.00 บาท แต่ยังนำมาลบเงินซื้อแล้วเรียกว่า “ผลตอบแทนต่อปี” ไม่ได้ เพราะได้รับเงินต่างเวลา หากอยากเปรียบเทียบกับเงินวันนี้ต้องคิดลด

เราจะใช้จำนวนบาทเต็มในตัวอย่าง ไม่ใช้ราคาที่เสนอเป็นเปอร์เซ็นต์ของ face value ตัวเลขราคา 97.2768 ต่อ face 100 จึงเท่ากับราคา 972.768 บาทสำหรับ face 1,000 บาท ต้องแปลงหน่วยให้ตรงก่อนคำนวณจำนวนหน่วยซื้อ

ราคาคือมูลค่าปัจจุบันของเงินรับทุกก้อน

ให้ CiC_i เป็นเงินรับในอีก tit_i ปี และ yy เป็น yield ต่อปีแบบ effective ราคาในแบบจำลองคือ

B(y)=∑iCi(1+y)ti.B(y)=\sum_i\frac{C_i}{(1+y)^{t_i}}.

ตัวอย่างนี้ใช้ flat yield กับทุก cash flow เมื่อ y=5%y=5\% เราหารคูปองปีแรกด้วย 1.05 หารปีที่สองด้วย 1.0521.05^2 และหารเงินปีสุดท้ายด้วย 1.0531.05^3

def bond_value(flows, annual_yield):
    if annual_yield <= -1:
        raise ValueError("Annual effective yield must exceed -100%.")
    times = flows.index.to_numpy(dtype=float)
    amounts = flows.to_numpy(dtype=float)
    return float(np.sum(amounts / (1 + annual_yield) ** times))

yield_rate = 0.05
price = bond_value(cashflows, yield_rate)
print(f"Price at 5% annual effective yield: {price:,.6f}")

ได้ราคา 972.767520 บาท ต่ำกว่า face value 1,000 บาท เพราะคูปอง 4% ต่ำกว่า yield 5% ที่ใช้คิดลด ผู้ซื้อได้รับทั้งคูปองและส่วนต่างระหว่างราคาซื้อกับเงินต้นที่ได้รับคืนเมื่อครบกำหนด

flows คือ Series เงินรับ ส่วน annual_yield คือทศนิยมของอัตราต่อปี return float(...) ส่งผลรวมกลับเป็นตัวเลขหนึ่งค่า เราใส่เงื่อนไข y>−1y>-1 เพื่อให้ฐานการทบต้นเป็นบวก ฟังก์ชันนี้เป็นตัวอย่างสำหรับ cash flow ที่ตรวจแล้วว่าครบและเป็นตัวเลข ไม่ใช่ระบบตรวจสัญญาตราสารหนี้ทุกแบบ

price_rows = []
for y in [0.03, 0.04, 0.05, 0.06, 0.07]:
    price_rows.append({"Yield": y, "Price": bond_value(cashflows, y)})
print(pd.DataFrame(price_rows).round(6))

ที่ yield 4% ราคาจะเท่ากับ 1,000 บาท เมื่อ yield สูงขึ้นแต่ cash flow เดิม ราคาเงินรับในอนาคตลดลง จึงทำให้ราคาพันธบัตรลดลง ส่วนเมื่อ yield ลดลงราคาจะสูงขึ้น กลไกนี้มาจากตัวหารในสูตร ไม่ต้องสมมติว่าผู้ออกเปลี่ยนคูปองตามราคาตลาด TreasuryDirect อธิบายความสัมพันธ์ของราคา ดอกเบี้ย และ yield

Yield to maturity สรุปเงินรับหลายงวดด้วยอัตราเดียว

ถ้ารู้ราคาตลาดแล้ว เราหา yield ที่ทำให้ PV ของเงินรับเท่ากับราคานั้นได้ เรียกว่า yield to maturity หรือ YTM ในตัวอย่างที่รู้เงินรับแน่นอน YTM เป็นอัตราผลตอบแทนภายในของกระแสเงินตามสัญญา

from scipy.optimize import brentq

observed_price = 972.7675197062952
recovered_yield = brentq(
    lambda y: bond_value(cashflows, y) - observed_price,
    0.0,
    0.20,
)
print(f"Yield recovered from price: {recovered_yield:.6%}")

ได้ 5.000000% lambda y: ... สร้างฟังก์ชันสั้นที่รับ yield แล้วคืนส่วนต่างระหว่างราคาคำนวณกับราคาที่กำหนด brentq หา yield ที่ทำให้ส่วนต่างเป็นศูนย์ภายในช่วง 0% ถึง 20% ตัวอย่างนี้เลือกช่วงที่ครอบคำตอบและมีเครื่องหมายของส่วนต่างต่างกันที่ปลายทั้งสอง

การรายงาน YTM ยังไม่รับรองว่าเงินทุกก้อนจะได้รับตามสัญญา และถ้าจะตีความว่าเงินลงทุนทั้งหมดโตทบต้นด้วย YTM จนถึงวันสุดท้าย ต้องนำคูปองที่ได้รับก่อนหน้าไปลงทุนต่อในอัตราที่สอดคล้องกันด้วย หากขายก่อนครบกำหนด ราคาขายจะขึ้นกับ yield ณ วันขาย FINRA: Bond Yield and Return

จ่ายคูปองบ่อยขึ้น ต้องรักษาหน่วยอัตราให้ตรง

พันธบัตร face 1,000 บาท coupon rate 4% ที่จ่ายปีละสองครั้งจะจ่ายคูปองครั้งละ 20 บาท ไม่ใช่ครั้งละ 40 บาท หากเหลือสามปี จะมีเงินรับหกงวดที่เวลา 0.5, 1.0, 1.5, 2.0, 2.5 และ 3.0 ปี

def coupon_cashflows(face, coupon, maturity, frequency):
    periods = maturity * frequency
    if frequency <= 0 or not np.isclose(periods, round(periods)):
        raise ValueError("Use a positive frequency and a whole coupon schedule.")
    n = int(round(periods))
    if n < 1:
        raise ValueError("At least one payment must remain.")
    amounts = np.full(n, face * coupon / frequency)
    amounts[-1] += face
    times = np.arange(1, n + 1) / frequency
    return pd.Series(amounts, index=times)

semiannual_flows = coupon_cashflows(1_000.0, 0.04, 3, 2)
print(semiannual_flows)

ห้างวดแรกเป็น 20 บาท และงวดสุดท้ายเป็น 1,020 บาท np.full(n, value) สร้าง array ความยาว n ที่ทุกตำแหน่งเริ่มด้วยค่าเดียวกัน amounts[-1] += face เพิ่มเงินต้นเข้าเฉพาะงวดสุดท้าย ส่วน np.arange(1, n + 1) นับเลขงวดจาก 1 ถึง 6

ตลาดอาจเสนอ yield เป็น nominal annual yield ที่ทบต้นปีละ mm ครั้ง ถ้า quoted yield เท่ากับ yqy_q อัตราต่องวดคือ yq/my_q/m และอัตราต่อปีแบบ effective คือ

yeff=(1+yqm)m−1.y_{\mathrm{eff}}=\left(1+\frac{y_q}{m}\right)^m-1.
quoted_yield = 0.05
frequency = 2
period_yield = quoted_yield / frequency
effective_yield = (1 + period_yield) ** frequency - 1

price_by_years = bond_value(semiannual_flows, effective_yield)
period_numbers = np.arange(1, len(semiannual_flows) + 1)
price_by_periods = np.sum(
    semiannual_flows.to_numpy() / (1 + period_yield) ** period_numbers
)
print(f"Effective annual yield: {effective_yield:.4%}")
print(f"Price using years: {price_by_years:.6f}")
print(f"Price using coupon periods: {price_by_periods:.6f}")
print(np.isclose(price_by_years, price_by_periods))

Nominal yield 5% ทบต้นครึ่งปีเท่ากับ effective yield 5.0625% สองวิธีให้ราคา 972.459373 บาทเท่ากัน ส่วน effective yield 5% พอดีต้องใช้อัตราครึ่งปี (1.05)1/2−1(1.05)^{1/2}-1 จึงจะตรงหน่วย การหาร 5% ด้วยสองโดยไม่ดู convention จะเปลี่ยนสมมติฐานของราคา

ตัวอย่างนี้ไม่มีงวดคูปองสั้นหรือยาวผิดปกติ หากซื้อระหว่างวันจ่ายคูปอง ต้องคำนวณ accrued interest และใช้วิธีนับวันของสัญญา Clean price เป็นราคาที่ไม่รวมดอกเบี้ยค้างรับ ส่วน dirty price รวมรายการนั้นแล้ว การใช้ราคาชนิดหนึ่งที่ต้นงวดและอีกชนิดที่ปลายงวดทำให้ total return ผิดได้

ผลตอบแทนต้องนับทั้งราคาและเงินที่จ่ายออกมา

หลังจ่ายคูปอง มูลค่าพันธบัตรส่วนที่เหลือจะไม่รวมเงินคูปองที่ผู้ถือได้รับไปแล้ว เราเรียกราคาแบบนี้ว่า ex-coupon price การคำนวณผลตอบแทนหนึ่งงวดต้องนำเงินรับระหว่างงวดกลับมารวม

Rt=Bt+CtBt−1−1.R_t=\frac{B_t+C_t}{B_{t-1}}-1.

ในสูตรนี้ CtC_t หมายถึงเงินรับทั้งหมดที่ปลายงวด รวมเงินคืนต้นถ้าเป็นงวดครบกำหนด หลังชำระเงินครบสัญญาแล้วตั้ง BT=0B_T=0 เพราะไม่มี cash flow ของพันธบัตรฉบับเดิมเหลืออยู่

valuation_times = np.array([0.0, 1.0, 2.0, 3.0])
ex_coupon_prices = []
for now in valuation_times:
    future = cashflows[cashflows.index > now]
    if len(future) == 0:
        ex_coupon_prices.append(0.0)
    else:
        remaining = pd.Series(
            future.to_numpy(), index=future.index.to_numpy() - now
        )
        ex_coupon_prices.append(bond_value(remaining, yield_rate))
ex_coupon_prices = np.array(ex_coupon_prices)
payments = np.array([0.0, 40.0, 40.0, 1_040.0])
holding_returns = (
    ex_coupon_prices[1:] + payments[1:]
) / ex_coupon_prices[:-1] - 1
print(pd.DataFrame({
    "Time": valuation_times,
    "Ex-coupon price": ex_coupon_prices,
    "Payment at time": payments,
}).round(6))
print("Annual holding returns:", holding_returns.round(6))

ได้ holding returns 0.05, 0.05, 0.05 เมื่อ yield คงที่ 5% และรับเงินตรงเวลา งวดสุดท้ายใช้ 0+1,0400+1{,}040 เป็นมูลค่ารับ ไม่ใช้ราคา 1,040 แล้วบวกคูปอง 40 ซ้ำอีกครั้ง

โค้ดใช้ [1:] เลือกตั้งแต่แถวแรกหลังวันเริ่ม และ [:-1] เลือกทุกแถวยกเว้นแถวสุดท้าย ทำให้จำนวนตัวตั้งและตัวหารเท่ากัน เราใช้ array ซึ่งจับคู่ตามตำแหน่ง หลังจากตรวจเวลาแล้ว หากใช้ Series ที่ติด index เดิม ต้องระวัง pandas จับคู่ด้วยป้ายกำกับ

terminal_with_reinvestment = 40 * 1.05 ** 2 + 40 * 1.05 + 1_040
terminal_without_interest = payments.sum()
print(f"Coupons reinvested at 5%: {terminal_with_reinvestment:,.2f}")
print(f"Coupons kept with zero interest: {terminal_without_interest:,.2f}")
print(f"Initial price compounded at 5%: {price * 1.05 ** 3:,.2f}")

ถ้านำคูปองไปลงทุนต่อที่ 5% เงินวันสุดท้ายรวม 1,126.10 บาท เท่ากับราคาซื้อทบต้น 5% สามปี หากพักคูปองไว้โดยไม่มีดอกเบี้ยจะรวมเพียง 1,120.00 บาท สูตร holding return บอกผลตอบแทนแต่ละงวด ส่วนการเชื่อมผลตอบแทนหลายงวดเป็นเส้น wealth ต้องบอกด้วยว่าจัดการเงินคูปองอย่างไร

Macaulay duration เป็นเวลาเฉลี่ยที่ถ่วงด้วย PV

พันธบัตรสามปีในตัวอย่างเริ่มคืนเงินบางส่วนตั้งแต่ปีแรก อายุครบกำหนดสามปีจึงต่างจากเวลาเฉลี่ยของเงินรับ เราคิดน้ำหนักของแต่ละงวดจาก PV ของเงินงวดนั้นหารด้วยราคาพันธบัตร

wi=Ci/(1+y)tiB(y),DMac=∑iwiti.w_i=\frac{C_i/(1+y)^{t_i}}{B(y)},\qquad D_{\mathrm{Mac}}=\sum_i w_i t_i.

น้ำหนักรวมเท่ากับหนึ่ง และเมื่อ tit_i วัดเป็นปี duration ก็มีหน่วยปี สูตรไม่ได้ถามว่าใช้เวลากี่ปีจึงได้เงินสดสะสมคืนเท่าราคาซื้อ แต่เป็นค่าเฉลี่ยวันรับเงินโดยใช้มูลค่าวันนี้เป็นน้ำหนัก

times = cashflows.index.to_numpy()
discounted_cash = cashflows.to_numpy() / (1 + yield_rate) ** times
pv_weights = discounted_cash / discounted_cash.sum()
macaulay = np.sum(times * pv_weights)
print(pd.DataFrame({
    "Years": times,
    "PV": discounted_cash,
    "PV weight": pv_weights,
    "Weighted years": times * pv_weights,
}).round(6))
print(f"Weight sum: {pv_weights.sum():.6f}")
print(f"Macaulay duration: {macaulay:.6f} years")

ได้ 2.884380 ปี ซึ่งสั้นกว่าอายุสามปีเล็กน้อย เพราะมีเงินรับปีแรกและปีที่สองด้วย สำหรับ ZCB ที่จ่ายเพียงครั้งเดียวในอีกห้าปี น้ำหนักทั้งหมดอยู่ปีที่ห้า Macaulay duration จึงเท่ากับห้าปี

Modified duration ใช้ประมาณการเปลี่ยนราคา

สำหรับ yield ต่อปีแบบ effective ในสูตรที่ใช้อยู่ การหาอนุพันธ์ของราคาทำให้ได้

Dmod=DMac1+y,ΔBB≈−DmodΔy.D_{\mathrm{mod}}=\frac{D_{\mathrm{Mac}}}{1+y},\qquad \frac{\Delta B}{B}\approx-D_{\mathrm{mod}}\Delta y.

ตัวอย่างนี้ modified duration เท่ากับประมาณ 2.747028 หาก yield เพิ่ม 0.1 percentage point จาก 5% เป็น 5.1% ต้องใช้ Δy=0.001\Delta y=0.001 จึงประมาณว่าราคาลดลงประมาณ 0.2747%

modified = macaulay / (1 + yield_rate)
yield_change = 0.001
approximate_change = -modified * yield_change
new_price = bond_value(cashflows, yield_rate + yield_change)
actual_change = new_price / price - 1
print(f"Modified duration: {modified:.6f}")
print(f"Approximate price change: {approximate_change:.4%}")
print(f"Actual repriced change: {actual_change:.4%}")

ราคาเมื่อคำนวณใหม่ลด 0.2742% ใกล้กับค่าประมาณ −0.2747% ความต่างเกิดจากเส้นราคาต่อยีลด์โค้ง ส่วน duration ใช้เส้นตรงประมาณใกล้จุดเดิม ถ้าขยับ yield มากขึ้น ความคลาดเคลื่อนอาจเพิ่มขึ้น จึงต้อง reprice ด้วย cash flow หรือใช้ convexity ช่วยเมื่อเหมาะสม

กรณี yield เป็น nominal annual ที่ทบต้น mm ครั้งต่อปี ความไวต่อ quoted yield จะเป็น DMac/(1+yq/m)D_{\mathrm{Mac}}/(1+y_q/m) เมื่อ Macaulay วัดเป็นปี ต้องระบุให้ครบว่ากำลังขยับ yield ชนิดใด การเปลี่ยน convention แล้วใช้ duration ตัวเดิมอาจทำให้หน่วยไม่ตรง CFA Institute อธิบาย money duration และการจับคู่ภาระ

จับคู่มูลค่าและ Duration ของภาระห้าปี

สมมติต้องจ่าย 100,000 บาทในอีกห้าปี และเส้นอัตราแบนที่ effective yield 5% มูลค่าภาระวันนี้เท่ากับ 78,352.62 บาท มี ZCB อายุสองปีและแปดปีให้ใช้ เราต้องการน้ำหนักตามมูลค่าตลาด ww ในตราสารสองปี และ 1−w1-w ในตราสารแปดปี

2w+8(1−w)=5⇒w=0.5.2w+8(1-w)=5\quad\Rightarrow\quad w=0.5.

แบ่งเงิน PV ของภาระครึ่งหนึ่งซื้อ ZCB สองปี และอีกครึ่งซื้อ ZCB แปดปี จำนวน face value ที่ได้รับเมื่อครบกำหนดจะไม่เท่ากัน เพราะราคาต่อเงินครบกำหนด 1 บาทต่างกัน

goal_amount = 100_000.0
goal_time = 5.0
base_yield = 0.05
goal_pv = goal_amount / (1 + base_yield) ** goal_time
short_time, long_time = 2.0, 8.0
short_weight = (long_time - goal_time) / (long_time - short_time)
short_face = goal_pv * short_weight * (1 + base_yield) ** short_time
long_face = goal_pv * (1 - short_weight) * (1 + base_yield) ** long_time

print(f"Liability PV: {goal_pv:,.6f}")
print(f"Short-bond market-value weight: {short_weight:.2%}")
print(f"Short bond face: {short_face:,.6f}")
print(f"Long bond face: {long_face:,.6f}")
print(f"Portfolio Macaulay duration: {short_weight * 2 + (1-short_weight) * 8:.2f}")

ได้ราคาเริ่ม 78,352.616647 บาท น้ำหนัก 50%/50% และ duration ห้าปีเหมือนภาระ เงื่อนไข PV เท่ากันทำให้จำนวนบาทที่เปลี่ยนจากการขยับ yield เล็กน้อยใกล้กันด้วย หากใช้พอร์ตที่ duration ห้าปีแต่ใส่เงินเพียงครึ่งหนึ่งของ PV ภาระ ความไวคิดเป็นบาทก็เหลือเพียงครึ่งเดียว

matching_rows = []
for y in [0.03, 0.04, 0.05, 0.06, 0.07]:
    liability = goal_amount / (1 + y) ** goal_time
    assets = short_face / (1 + y) ** short_time + long_face / (1 + y) ** long_time
    matching_rows.append({
        "Yield": y,
        "Matched assets": assets,
        "Liability PV": liability,
        "Matched funding ratio": assets / liability,
        "Cash funding ratio": goal_pv / liability,
    })
matching_results = pd.DataFrame(matching_rows)
print(matching_results.round(6).to_string(index=False))
Yield หลังขยับทั้งเส้น PV สินทรัพย์จับคู่ PV ภาระ Funding ratio
3% 86,404.48 86,260.88 100.1665%
4% 82,226.58 82,192.71 100.0412%
5% 78,352.62 78,352.62 100.0000%
6% 74,756.03 74,725.82 100.0404%
7% 71,412.88 71,298.62 100.1603%

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

Funding ratio ของพอร์ตจับคู่ Duration เทียบกับเงินสด เมื่อ Yield เลื่อนพร้อมกันจาก 3 ถึง 7 เปอร์เซ็นต์
ข้อมูลสมมติจากตัวอย่างเดียวกัน: ทั้งสองพอร์ตเริ่มด้วยเงินเท่ากับ PV ของภาระ 100,000 บาทในอีกห้าปีที่ yield 5% พอร์ต ZCB อายุสองและแปดปีแบ่ง PV เท่ากันจึงมี Macaulay duration ห้าปี เส้นที่อยู่ใกล้ 100% แสดงผลของการจับคู่ภายใต้การเลื่อน yield พร้อมกัน; กราฟนี้ยังไม่รวมการเปลี่ยนรูปทรงเส้นอัตราดอกเบี้ย

Duration เท่ากันยังไม่ทำให้ cash flow ตรงกัน

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

ถ้าเส้นอัตราไม่ได้ขยับขนานกัน การจับคู่ด้วย duration ตัวเดียวอาจไม่ครอบคลุม ลองให้ yield สองปีและห้าปียังคง 5% แต่ yield แปดปีเพิ่มเป็น 7%

twisted_assets = short_face / 1.05 ** 2 + long_face / 1.07 ** 8
twisted_liability = goal_amount / 1.05 ** 5
print(f"Assets after curve twist: {twisted_assets:,.2f}")
print(f"Funding ratio after curve twist: {twisted_assets / twisted_liability:.4%}")

สินทรัพย์เหลือ 72,863.72 บาท และ funding ratio ลดเป็น 92.9946% เพราะตราสารแปดปีลดราคา ขณะที่ PV ของภาระห้าปีไม่เปลี่ยน การจัดการเส้นอัตราหลายช่วงอาจใช้ key-rate durations หรือจับคู่ cash flow ละเอียดขึ้น การใช้ตราสารหลายอายุอย่างเดียวไม่ได้รับรองว่าจับคู่ความเสี่ยงเหล่านี้แล้ว

ยังมีความเสี่ยงจากการผิดนัด การเปลี่ยน credit spread สภาพคล่อง การไถ่ถอนก่อนกำหนด และจำนวนภาระที่เปลี่ยน หาก cash flow เปลี่ยนตามดอกเบี้ย เช่น พันธบัตร callable สูตร duration ของ cash flow คงที่ในบทนี้ต้องปรับ วิธีคำนวณที่เหมาะสมจะต้อง reprice ภายใต้เงื่อนไขสัญญาใหม่ด้วย

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

  1. Face value 1,000 บาท coupon rate 6% จ่ายครึ่งปี คูปองต่องวดเป็นเท่าไร?
  2. ZCB จ่าย 1,000 บาทในอีกสองปี yield effective 4% ราคาและ Macaulay duration เท่าไร?
  3. Modified duration เท่ากับ 6 ถ้า yield ขึ้น 0.25 percentage point ราคาจะเปลี่ยนประมาณกี่เปอร์เซ็นต์?
  4. ต้องการ duration สี่ปีจาก ZCB สองปีกับหกปี ควรใช้สัดส่วนมูลค่าตลาดเท่าไร?
  5. พันธบัตรถึงวันครบกำหนดและจ่ายเงินต้นพร้อมคูปองแล้ว เหตุใดจึงไม่ควรเก็บเงินต้นไว้ทั้งในราคาปลายงวดและใน cash payment?
  6. หาก matching ด้วย duration ทำให้พอร์ตได้ผลดีเมื่ออัตราทุกอายุขยับพร้อมกัน แปลว่าป้องกันการ steepen ของเส้นอัตราแล้วหรือไม่?
เปิดวิธีคำนวณและตรวจหน่วย

ข้อ 1 คูปองเท่ากับ 1,000(0.06)/2=301{,}000(0.06)/2=30 บาทต่องวด ข้อ 2 ราคา 1,000/1.042≈924.561{,}000/1.04^2\approx924.56 บาท และ Macaulay duration สองปี ส่วน modified duration ต่อ effective yield เท่ากับ 2/1.04≈1.92312/1.04\approx1.9231

ข้อ 3 ใช้ Δy=0.0025\Delta y=0.0025 จึงประมาณ −6(0.0025)=−1.5%-6(0.0025)=-1.5\% ข้อ 4 แก้ 2w+6(1−w)=42w+6(1-w)=4 ได้ w=0.5w=0.5 แล้วต้องใส่เงินรวมให้ PV สอดคล้องกับภาระด้วย

ข้อ 5 การนับทั้งสองแห่งทำให้เงินรับเกินจริง หลังชำระครบสัญญา ex-coupon price เป็นศูนย์ เงินต้นและคูปองอยู่ใน cash payment ครั้งเดียว ข้อ 6 ยังสรุปไม่ได้ เพราะอัตราต่างอายุอาจขยับต่างทิศหรือคนละขนาด ต้องทดสอบรูปแบบ curve change นั้นโดยตรง

แหล่งเรียนและการอ่าน Lab

ต้นทางคือหัวข้อ Liability hedging portfolios, Lab Session–Liability driven investing และ Lab Session–Monte Carlo simulation of coupon-bearing bonds using CIR ประกอบ lab_126.ipynb และ lab_127.ipynb ที่อ่านในเครื่อง

Lab ใช้เลขงวดคูปองในบางฟังก์ชัน และใช้อัตราต่องวด จึงต้องแปลง duration จากจำนวนงวดเป็นปีให้ตรงความหมาย ส่วนตัวอย่างหน้านี้ใช้เวลาเป็นปีและระบุ effective yield เป็นหลัก นอกจากนี้เรากำหนดราคาหลังชำระครบสัญญาให้เป็นศูนย์แล้วบันทึกเงินคืนต้นใน cash payment เพื่อป้องกันการนับเงินก้อนสุดท้ายซ้ำ

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