債券到期收益率小程式

話說過年前的目標還包括撿便宜股票這部分, 果真大陸同胞藉著 DeepSeek 賜我一個低點, 這怎麼能不好好把握呢?

2025/1/27, Nvidia 大跌那天晚上, 老婆在旁邊慫恿我說: 子時算第二天, 要不要早點拜拜, 這樣明天早餐就可以吃拜完的年貨了. 我一面看著 QQQ 跌到 512, 一面拒絕她. “哪有人半夜拜拜, 又不是吃宵夜.". 稍微分心一下, QQQ 又漲回 514.

“再胡思亂想可能就要錯過行情啦~~~", 我原始的爬蟲腦提醒我. 於是我看著帳戶裡的數字. 用剛從澳洲玩回來的數學邏輯, 掐指一算, 這個乘 20 倍就是 OO 台幣. 好, 全梭了! 哈! 市價買進,瞬間成交!

But, 過年比較晚睡, 混了幾個小時, 我忽然覺醒了! 诶? 美金不是應該 x 32 嗎? 那不是 OO 是 OOO 啊! 就這樣, 我下了史上最多位數台幣的買單, 以後也不太可能再突破了.

市值型我應該算買好買滿了. 接下來要思考配息收入. 畢竟入 QQQ 這大門以後, 只有把 PFF 當作固定收益的來源. 它真的是最好的嗎? 年假也是一個可以研究的好時間. However, 現在配息股票, 債券 ETF 五花八門, 看得我眼花撩亂. 以前還可以靠網龍大富翁選個股, 現在要靠什麼呢?

在網路上逛了一下, 的確有 ETF 篩選器. 但它們的標的, 不是電子就是金融為主, 兩個我都不太喜歡. 債券方面, 網路上有免費的 YTM (Yield to Maturity) 計算機, 不過限制很多, 根本不實用. 難道這些人不知道現在軟體工程師最不值錢嗎? 我乾脆叫 AI “寫" 了一個. 附在後面. 包括 debug 也是叫 AI 自己反省! “為什麼算出來比標準答案低?" 它就會自己找出哪裡邏輯錯誤了, 哈!

其中, Clean YTM不包括前手息. Dirty YTM 包含前手息 – 要補償即時的債息給前一手賣家, 所以算出來會低一點. 到期日可以是 X.Y 年, 有的免費網頁上居然要求只能輸入整數, 那半年配不是還要動腦四捨五入才知道配息幾期? 這種事叫電腦做就好了啊!

程式寫好之後. …. 其實唯一的用途是: 證明證券公司寫的預期收益率是 clean TYM, 如果加計前手息, 即使兩者只差一點點, 但想要 4% 收益率, 實際只有 3.99% 會覺得不完美. ^^

[後記] 加碼用 Bolt [1] 做成網頁, 前後不到 3 分鐘.

https://aquamarine-peony-b9fe05.netlify.app/

[REF]

  1. https://dessign.net/bolt-new-ai/
  2. 原始檔
from typing import Literal, Tuple
import numpy as np
from datetime import datetime, date

class BondCalculator:
    def __init__(
        self,
        face_value: float,
        coupon_rate: float,
        years_to_maturity: float,
        clean_price: float,
        payment_frequency: Literal['A', 'S', 'M'] = 'S',
        last_coupon_date: date = None,
        settlement_date: date = None,
        day_count_convention: Literal['30/360', 'ACT/365', 'ACT/ACT'] = '30/360'
    ):
        """
        債券計算器
        
        參數:
            face_value: 面額
            coupon_rate: 票面利率(年利率)
            years_to_maturity: 到期年數
            clean_price: 淨價
            payment_frequency: 配息頻率 'A'=年配, 'S'=半年配, 'M'=月配
            last_coupon_date: 上一次配息日
            settlement_date: 交割日
            day_count_convention: 計息基礎 '30/360', 'ACT/365', 'ACT/ACT'
        """
        self.face_value = face_value
        self.coupon_rate = coupon_rate
        self.years_to_maturity = years_to_maturity
        self.clean_price = clean_price
        self.payment_frequency = payment_frequency
        self.last_coupon_date = last_coupon_date
        self.settlement_date = settlement_date
        self.day_count_convention = day_count_convention
        
        self.freq_map = {'A': 1, 'S': 2, 'M': 12}
        self.freq = self.freq_map[payment_frequency]
        self.n_payments = round(years_to_maturity * self.freq)
        self.period_coupon = face_value * coupon_rate / self.freq
        
        # 計算前手息
        self.accrued_interest = self._calculate_accrued_interest() if (last_coupon_date and settlement_date) else 0
        self.dirty_price = self.clean_price + self.accrued_interest
        
    def _calculate_accrued_interest(self) -> float:
        """計算前手息"""
        # 計算天數
        if self.day_count_convention == '30/360':
            y1, m1, d1 = self.last_coupon_date.year, self.last_coupon_date.month, self.last_coupon_date.day
            y2, m2, d2 = self.settlement_date.year, self.settlement_date.month, self.settlement_date.day
            
            if d1 == 31: d1 = 30
            if d2 == 31 and d1 == 30: d2 = 30
            
            days = (360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1))
            days_in_year = 360
            
        elif self.day_count_convention == 'ACT/365':
            days = (self.settlement_date - self.last_coupon_date).days
            days_in_year = 365
            
        else:  # 'ACT/ACT'
            days = (self.settlement_date - self.last_coupon_date).days
            if self.settlement_date.year == self.last_coupon_date.year:
                days_in_year = 366 if self.settlement_date.year % 4 == 0 else 365
            else:
                days_in_year = 365.25
        
        return self.period_coupon * (days / (days_in_year / self.freq))
    
    def _bond_price(self, ytm: float, price_type: Literal['clean', 'dirty'] = 'dirty') -> float:
        """計算債券價格"""
        r = ytm / self.freq
        print(self.n_payments)
        periods = np.arange(1, self.n_payments + 1)
        #print(periods)
        
        # 計算所有配息現值
        pv_coupons = sum(self.period_coupon / (1 + r) ** t for t in periods)
        # 計算面額現值
        pv_face = self.face_value / (1 + r) ** self.n_payments
        
        # 如果計算淨價,需要扣除前手息
        if price_type == 'clean':
            return pv_coupons + pv_face - self.accrued_interest
        return pv_coupons + pv_face
    
    def _bond_price_derivative(self, ytm: float) -> float:
        """計算價格對YTM的導數"""
        r = ytm / self.freq
        periods = np.arange(1, self.n_payments + 1)
        
        d_coupons = sum(-t * self.period_coupon / ((1 + r) ** (t + 1)) for t in periods) / self.freq
        d_face = -self.n_payments * self.face_value / ((1 + r) ** (self.n_payments + 1)) / self.freq
        return d_coupons + d_face
    
    def calculate_ytm(self) -> Tuple[float, float]:
        def newton_method(target_price: float) -> float:
            ytm = self.coupon_rate
            tolerance = 1e-7
            max_iter = 100
            
            for _ in range(max_iter):
                price_diff = self._bond_price(ytm) - target_price
                if abs(price_diff) < tolerance:
                    break
                ytm = ytm - price_diff / self._bond_price_derivative(ytm)
            
            return ytm
        
        # 修正:直接用淨價計算clean YTM
        clean_ytm = newton_method(self.clean_price)
        # 用總價計算dirty YTM
        dirty_ytm = newton_method(self.dirty_price)
        
        return clean_ytm, dirty_ytm
    

# 使用範例
if __name__ == "__main__":
    # 測試案例
    calculator = BondCalculator(
        face_value=100,      # 面額
        coupon_rate=0.04375,      # 票面利率 5%
        years_to_maturity=9.25, # 到期年數
        clean_price=102.23,      # 淨價
        payment_frequency='S',  # 半年配
        last_coupon_date=date(2024, 11, 15),  # 上次配息日
        settlement_date=date(2025, 1, 1),    # 交割日
        day_count_convention='ACT/360'         # 計息基礎
    )
    
    clean_ytm, dirty_ytm = calculator.calculate_ytm()
    
    print("債券計算結果:")
    print(f"面額: {calculator.face_value:,.2f}")
    print(f"票面利率: {calculator.coupon_rate:.3%}")
    print(f"到期年數: {calculator.years_to_maturity:.2f}年")
    print(f"配息頻率: {calculator.payment_frequency}")
    print(f"\n前手息: {calculator.accrued_interest:.2f}")
    print(f"淨價: {calculator.clean_price:.2f}")
    print(f"總價: {calculator.dirty_price:.2f}")
    print(f"\n淨價收益率 (Clean YTM): {clean_ytm:.4%}")
    print(f"總價收益率 (Dirty YTM): {dirty_ytm:.4%}")
    print(f"收益率差異: {(clean_ytm - dirty_ytm):.4%}")

發表留言