定期付息债券应计利息

定期付息债券应计利息 · ISDA(国际通用) 给定发行日、结算日、首息日等计算应计利息
根据债券发行日、结算日、首次付息日、面值、息票率等计算定期付息债券的应计利息。
格式 yyyy-mm-dd
格式 yyyy-mm-dd
格式 yyyy-mm-dd
计算结果
点击「计算」查看输出
code 计算逻辑代码
以下为「ISDA国际通用」QuantLib 代码示例。

# 依赖版本:
# python==3.9
# QuantLib==1.43
from datetime import datetime
import traceback
import QuantLib as ql


# ============================================================
# 工具函数
# ============================================================
def _parse_date(s: str) -> ql.Date:
    """解析 'yyyy-mm-dd' 字符串为 ql.Date"""
    d = datetime.strptime(s.strip(), "%Y-%m-%d")
    return ql.Date(d.day, d.month, d.year)


def _map_period(period: int):
    """年付息次数 -> (ql.Period, ql.Frequency)"""
    mapping = {
        0:  (None,                     ql.NoFrequency),
        1:  (ql.Period(ql.Annual),     ql.Annual),
        2:  (ql.Period(ql.Semiannual), ql.Semiannual),
        3:  (ql.Period(4, ql.Months),  ql.EveryFourthMonth),
        4:  (ql.Period(ql.Quarterly),  ql.Quarterly),
        6:  (ql.Period(ql.Bimonthly),  ql.Bimonthly),
        12: (ql.Period(ql.Monthly),    ql.Monthly),
    }
    if period not in mapping:
        raise ValueError(f"不支持的 Period: {period}")
    return mapping[period]


def _map_basis(basis: int):
    """日计数基准编号 -> ql.DayCounter"""
    mapping = {
        0:  ql.ActualActual(ql.ActualActual.ISDA),
        1:  ql.Thirty360(ql.Thirty360.USA),
        2:  ql.Actual360(),
        3:  ql.Actual365Fixed(),
        4:  ql.Thirty360(ql.Thirty360.BondBasis),
        5:  ql.Thirty360(ql.Thirty360.ISDA),
        6:  ql.Thirty360(ql.Thirty360.European),
        7:  ql.Actual365Fixed(),
        8:  ql.ActualActual(ql.ActualActual.ISMA),
        9:  ql.Actual360(),
        10: ql.Actual365Fixed(),
        11: ql.Thirty360(ql.Thirty360.EurobondBasis),
        12: ql.Actual365Fixed(),
        13: ql.Business252(),
    }
    if basis not in mapping:
        raise ValueError(f"暂不支持的 Basis: {basis}")
    return mapping[basis]


# ============================================================
# 核心计算函数
# ============================================================
def qjquant_calu_acrubond(
    issue_date: str,
    settle_date: str,
    first_coupon_date: str,
    face: float,
    coupon_rate: float,
    period: int = 2,
    basis: int = 0,
):
    """
    定期付息债券的应计利息
    对应 MATLAB: AccruInterest = acrubond(IssueDate, Settle, FirstCouponDate, Face, CouponRate, period, basis)
    """
    try:
        # ---- 参数校验(仅必要的) ----
        if coupon_rate < 0:
            return {"ok": False, "msg": "息票率不能为负"}
        if face <= 0:
            return {"ok": False, "msg": "面值必须大于 0"}

        issue_qd        = _parse_date(issue_date)
        settle_qd       = _parse_date(settle_date)
        first_coupon_qd = _parse_date(first_coupon_date)

        # 关键修正:Settle 只需晚于 IssueDate,不需要早于 FirstCouponDate
        if issue_qd >= settle_qd:
            return {"ok": False, "msg": "发行日必须早于结算日"}
        if first_coupon_qd <= issue_qd:
            return {"ok": False, "msg": "首次付息日必须晚于发行日"}

        coupon_period, freq = _map_period(period)
        day_counter = _map_basis(basis)

        # ---- 零息:无定期付息,应计利息为 0 ----
        if period == 0:
            return {"ok": True, "accru_interest": 0.0}

        # ---- 构造完整付息时间表 ----
        # 首息期由 IssueDate -> FirstCouponDate 决定(可能是标准/短期/长期首息期)。
        # 之后以 FirstCouponDate 为基准,按 coupon_period 向后滚动。
        # 需要一直滚动到最后一个严格大于 Settle 的付息日之后,
        # 以确保 BondFunctions.accruedAmount 能定位到 Settle 所在的付息区间。
        dates = [issue_qd, first_coupon_qd]
        next_date = first_coupon_qd + coupon_period
        # 至少覆盖到 Settle 之后两个周期
        target = settle_qd + coupon_period * 2
        while next_date <= target:
            dates.append(next_date)
            next_date = next_date + coupon_period

        schedule = ql.Schedule(dates)

        bond = ql.FixedRateBond(
            settlementDays=0,
            faceAmount=face,
            schedule=schedule,
            coupons=[coupon_rate] * (len(dates) - 1),
            paymentDayCounter=day_counter,
        )

        accrued = ql.BondFunctions.accruedAmount(bond, settle_qd)

        return {
            "ok": True,
            "accru_interest": round(accrued, 4),
        }

    except ValueError as ve:
        traceback.print_exc()
        return {"ok": False, "msg": f"参数错误: {ve}"}
    except Exception as e:
        traceback.print_exc()
        return {"ok": False, "msg": f"未知错误: {e}"}


# ============================================================
# 示例:复制本文件后直接运行
# ============================================================
if __name__ == "__main__":
    print("=" * 60)
    print("示例 1:半年付息,实际/实际(结算日在首息期之内)")
    print("=" * 60)
    r1 = qjquant_calu_acrubond(
        issue_date="1983-01-31",
        settle_date="1993-03-01",
        first_coupon_date="1983-07-31",
        face=100.0,
        coupon_rate=0.1,
        period=2,
        basis=0,
    )
    print(r1)

    print()
    print("=" * 60)
    print("示例 2:年付息,30/360 USA(结算日在第二个付息周期)")
    print("=" * 60)
    r2 = qjquant_calu_acrubond(
        issue_date="2024-01-15",
        settle_date="2025-06-20",
        first_coupon_date="2025-01-15",
        face=100.0,
        coupon_rate=0.05,
        period=1,
        basis=1,
    )
    print(r2)

    print()
    print("=" * 60)
    print("示例 3:季付,实际/365 Fixed(结算日在首息期之内)")
    print("=" * 60)
    r3 = qjquant_calu_acrubond(
        issue_date="2024-01-15",
        settle_date="2024-04-10",
        first_coupon_date="2024-04-15",
        face=100.0,
        coupon_rate=0.04,
        period=4,
        basis=3,
    )
    print(r3)

    print()
    print("=" * 60)
    print("示例 4:零息债(period=0,应计利息恒为 0)")
    print("=" * 60)
    r4 = qjquant_calu_acrubond(
        issue_date="2024-01-15",
        settle_date="2024-05-20",
        first_coupon_date="2024-07-15",
        face=100.0,
        coupon_rate=0.055,
        period=0,
        basis=0,
    )
    print(r4)