You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java实现Excel YIELD函数结果与预期不符,求修正方案

实现与Excel YIELD函数一致的Java函数问题

问题背景

需要实现与Excel YIELD函数计算逻辑完全一致的Java函数,但测试时结果不符:

  • 测试参数:结算日2025/04/21、到期日2027/12/08、票面利率0.06875、价格94.28、赎回价值100、付息频率1
  • 代码输出:9.499084%
  • Excel计算结果:9.723637%

原实现代码

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

public class BondYieldCalculator {
    public static BigDecimal rendementTitre(
                LocalDate settlementDate,
                LocalDate maturityDate,
                BigDecimal taux,
                BigDecimal prix,
                BigDecimal valeurNominale,
                int frequence,
                MathContext mc,
                BigDecimal tol,
                int maxIter
        ) {
            long daysBetween = ChronoUnit.DAYS.between(settlementDate, maturityDate);
            BigDecimal years = new BigDecimal(daysBetween).divide(BigDecimal.valueOf(365), mc);
            int n = years.multiply(BigDecimal.valueOf(frequence)).setScale(0, RoundingMode.HALF_UP).intValue();
            BigDecimal coupon = valeurNominale.multiply(taux).divide(BigDecimal.valueOf(frequence), mc);

            // Initial guess
            BigDecimal y = taux;

            for (int iter = 0; iter < maxIter; iter++) {
                BigDecimal f = bondPrice(y, n, coupon, valeurNominale, frequence, mc).subtract(prix, mc);
                BigDecimal fPrime = bondPriceDerivative(y, n, coupon, valeurNominale, frequence, mc);

                if (fPrime.abs().compareTo(new BigDecimal("1E-10")) < 0) break;

                BigDecimal yNew = y.subtract(f.divide(fPrime, mc), mc);

                if (y.subtract(yNew).abs().compareTo(tol) < 0) {
                    System.out.println("Rendement : " + yNew + " YY%");
                    return yNew.multiply(BigDecimal.valueOf(100)).setScale(6, RoundingMode.HALF_UP); // % avec 6 décimales
                }

                y = yNew;
            }

            throw new RuntimeException("La convergence n'a pas été atteinte");
        }

        private static BigDecimal bondPrice(BigDecimal y, int n, BigDecimal C, BigDecimal V, int f, MathContext mc) {
            BigDecimal total = BigDecimal.ZERO;
            BigDecimal onePlusRate = BigDecimal.ONE.add(y.divide(BigDecimal.valueOf(f), mc));

            for (int i = 1; i <= n; i++) {
                BigDecimal denom = onePlusRate.pow(i, mc);
                total = total.add(C.divide(denom, mc), mc);
            }

            BigDecimal lastDenom = onePlusRate.pow(n, mc);
            total = total.add(V.divide(lastDenom, mc), mc);
            return total;
        }

        private static BigDecimal bondPriceDerivative(BigDecimal y, int n, BigDecimal C, BigDecimal V, int f, MathContext mc) {
            BigDecimal total = BigDecimal.ZERO;
            BigDecimal onePlusRate = BigDecimal.ONE.add(y.divide(BigDecimal.valueOf(f), mc));

            for (int i = 1; i <= n; i++) {
                BigDecimal denom = onePlusRate.pow(i + 1, mc);
                BigDecimal term = BigDecimal.valueOf(-i).multiply(C).divide(BigDecimal.valueOf(f), mc).divide(denom, mc);
                total = total.add(term, mc);
            }

            BigDecimal lastTerm = BigDecimal.valueOf(-n).multiply(V).divide(BigDecimal.valueOf(f), mc)
                    .divide(onePlusRate.pow(n + 1, mc), mc);
            total = total.add(lastTerm, mc);

            return total;
        }

    public static void main(String[] args) {
            LocalDate settlement = LocalDate.of(2025, 4, 21);
            LocalDate maturity = LocalDate.of(2027, 12, 8);

            BigDecimal taux = new BigDecimal("0.06875");
            BigDecimal prix = new BigDecimal("94.28");
            BigDecimal valeurNominale = new BigDecimal("100");

            MathContext mc = new MathContext(20, RoundingMode.HALF_UP);
            BigDecimal tol = new BigDecimal("1E-8");

            BigDecimal rendement = rendementTitre(
                    settlement,
                    maturity,
                    taux,
                    prix,
                    valeurNominale,
                    1,  // 年付
                    mc,
                    tol,
                    100
            );

            System.out.println("Rendement : " + rendement + " %");
    }
}

问题根源

原代码的核心错误在于完全忽略了Excel YIELD函数的精确时间计算规则,具体包括:

  1. 日计数基准错误:Excel YIELD默认使用US (NASD) 30/360日计数法,而非实际天数/365。
  2. 未处理非完整付息周期:原代码直接将总天数折算为整数个付息周期,忽略了结算日到下一个付息日的非完整周期,以及应计利息的影响。
  3. 全价/净价混淆:Excel YIELD的price参数是净价,需要加上应计利息得到全价后再进行折现计算,原代码直接用净价参与折现。
  4. 付息日期计算错误:未正确推导上一个付息日、下一个付息日,导致周期计算完全偏离YIELD的逻辑。

修正方案

要实现与Excel一致的YIELD计算,必须严格遵循以下步骤:

  1. 计算付息日期序列:根据到期日和付息频率,倒推上一个付息日(PCD)、下一个付息日(NCD),以及后续所有付息日。
  2. 应用30/360日计数法:计算PCD到结算日的天数(A)、PCD到NCD的天数(E)、结算日到NCD的天数(DSC)、PCD到期日的总天数(T)。
  3. 计算应计利息:accruedInterest = (票面利率/付息频率) * (A/E)。
  4. 调整为全价:fullPrice = price + accruedInterest。
  5. 迭代求解收益率:使用牛顿-拉夫逊法,基于全价和精确的折现周期(包括第一个非完整周期)计算收益率。

修正后的代码

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;

public class ExcelYieldCalculator {
    // 默认采用Excel YIELD的US (NASD) 30/360日计数基准
    private static final int DAY_COUNT_BASIS_US_30_360 = 0;

    public static BigDecimal calculateYield(
            LocalDate settlementDate,
            LocalDate maturityDate,
            BigDecimal couponRate,
            BigDecimal price,
            BigDecimal redemptionValue,
            int frequency,
            MathContext mc,
            BigDecimal tolerance,
            int maxIterations
    ) {
        // 1. 计算上一个付息日(PCD)和下一个付息日(NCD)
        LocalDate pcd = getPreviousCouponDate(maturityDate, settlementDate, frequency);
        LocalDate ncd = getNextCouponDate(pcd, frequency);

        // 2. 用30/360计算天数
        long daysPCDToSettlement = calculateDays30360(pcd, settlementDate);
        long daysPCDToNCD = calculateDays30360(pcd, ncd);
        long daysSettlementToNCD = daysPCDToNCD - daysPCDToSettlement;
        long daysPCDToMaturity = calculateDays30360(pcd, maturityDate);

        // 3. 计算应计利息
        BigDecimal couponPerPeriod = redemptionValue.multiply(couponRate).divide(BigDecimal.valueOf(frequency), mc);
        BigDecimal accruedInterest = couponPerPeriod.multiply(BigDecimal.valueOf(daysPCDToSettlement))
                .divide(BigDecimal.valueOf(daysPCDToNCD), mc);
        BigDecimal fullPrice = price.add(accruedInterest, mc);

        // 4. 计算付息周期数:从NCD到到期日的完整周期数
        int numFullPeriods = getFullPeriodsFromNCDToMaturity(ncd, maturityDate, frequency);

        // 5. 初始猜测收益率,Excel通常用票面利率或基于价格的估算
        BigDecimal y = couponRate;
        if (price.compareTo(redemptionValue) < 0) {
            y = couponRate.multiply(BigDecimal.valueOf(1.1), mc);
        } else {
            y = couponRate.multiply(BigDecimal.valueOf(0.9), mc);
        }

        // 6. 牛顿-拉夫逊迭代求解
        for (int iter = 0; iter < maxIterations; iter++) {
            BigDecimal ratePerPeriod = y.divide(BigDecimal.valueOf(frequency), mc);
            BigDecimal onePlusRate = BigDecimal.ONE.add(ratePerPeriod, mc);

            // 计算当前收益率下的债券全价
            BigDecimal presentValue = BigDecimal.ZERO;

            // 第一个非完整周期的折现因子:(1+r)^(-d/D)
            BigDecimal exponent = BigDecimal.valueOf(-daysSettlementToNCD).divide(BigDecimal.valueOf(daysPCDToNCD), mc);
            BigDecimal firstDiscountFactor = BigDecimal.valueOf(Math.pow(onePlusRate.doubleValue(), exponent.doubleValue()));

            // 折现第一个付息
            presentValue = presentValue.add(couponPerPeriod.multiply(firstDiscountFactor, mc), mc);

            // 折现后续完整周期的付息
            for (int i = 1; i <= numFullPeriods; i++) {
                BigDecimal discountFactor = onePlusRate.pow(-(i + 1), mc);
                presentValue = presentValue.add(couponPerPeriod.multiply(discountFactor, mc), mc);
            }

            // 折现赎回价值
            BigDecimal maturityExponent = BigDecimal.valueOf(-(numFullPeriods + daysSettlementToNCD / (double) daysPCDToNCD));
            BigDecimal maturityDiscountFactor = BigDecimal.valueOf(Math.pow(onePlusRate.doubleValue(), maturityExponent.doubleValue()));
            presentValue = presentValue.add(redemptionValue.multiply(maturityDiscountFactor, mc), mc);

            // 计算差值:当前全价 - 目标全价
            BigDecimal f = presentValue.subtract(fullPrice, mc);

            // 计算导数(价格对收益率的导数)
            BigDecimal fPrime = BigDecimal.ZERO;

            // 第一个周期的导数项
            BigDecimal firstDerivTerm = couponPerPeriod.multiply(exponent, mc)
                    .multiply(firstDiscountFactor, mc)
                    .divide(onePlusRate, mc)
                    .negate(mc);
            fPrime = fPrime.add(firstDerivTerm, mc);

            // 完整周期的导数项
            for (int i = 1; i <= numFullPeriods; i++) {
                BigDecimal periodExponent = BigDecimal.valueOf(-(i + 1));
                BigDecimal discountFactor = onePlusRate.pow(periodExponent.intValue(), mc);
                BigDecimal derivTerm = couponPerPeriod.multiply(periodExponent, mc)
                        .multiply(discountFactor, mc)
                        .divide(onePlusRate, mc)
                        .negate(mc);
                fPrime = fPrime.add(derivTerm, mc);
            }

            // 赎回价值的导数项
            BigDecimal maturityDerivTerm = redemptionValue.multiply(maturityExponent, mc)
                    .multiply(maturityDiscountFactor, mc)
                    .divide(onePlusRate, mc)
                    .negate(mc);
            fPrime = fPrime.add(maturityDerivTerm, mc);

            // 避免除以0
            if (fPrime.abs().compareTo(new BigDecimal("1E-10")) < 0) {
                break;
            }

            // 更新收益率
            BigDecimal yNew = y.subtract(f.divide(fPrime, mc), mc);

            // 检查收敛
            if (y.subtract(yNew).abs().compareTo(tolerance) < 0) {
                return yNew.multiply(BigDecimal.valueOf(100)).setScale(6, RoundingMode.HALF_UP);
            }

            y = yNew;
        }

        throw new RuntimeException("迭代未收敛");
    }

    // 倒推上一个付息日
    private static LocalDate getPreviousCouponDate(LocalDate maturityDate, LocalDate settlementDate, int frequency) {
        LocalDate pcd = maturityDate;
        int monthsPerPeriod = 12 / frequency;
        do {
            pcd = pcd.minusMonths(monthsPerPeriod);
        } while (pcd.isAfter(settlementDate));
        return pcd;
    }

    // 获取下一个付息日
    private static LocalDate getNextCouponDate(LocalDate pcd, int frequency) {
        int monthsPerPeriod = 12 / frequency;
        return pcd.plusMonths(monthsPerPeriod);
    }

    // 计算NCD到到期日的完整付息周期数
    private static int getFullPeriodsFromNCDToMaturity(LocalDate ncd, LocalDate maturityDate, int frequency) {
        int count = 0;
        LocalDate current = ncd;
        int monthsPerPeriod = 12 / frequency;
        while (current.isBefore(maturityDate)) {
            current = current.plusMonths(monthsPerPeriod);
            if (!current.isAfter(maturityDate)) {
                count++;
            }
        }
        return count;
    }

    // 实现US (NASD) 30/360日计数法
    private static long calculateDays30360(LocalDate start, LocalDate end) {
        int startDay = start.getDayOfMonth();
        int startMonth = start.getMonthValue();
        int startYear = start.getYear();

        int endDay = end.getDayOfMonth();
        int endMonth = end.getMonthValue();
        int endYear = end.getYear();

        // 调整规则:如果起始日是31日,改为30日;如果结束日是31日且起始日不是30或31日,改为30日
        if (startDay == 31) {
            startDay = 30;
        }
        if (endDay == 31 && startDay != 30 && startDay != 31) {
            endDay = 30;
        }

        return (endYear - startYear) * 360 + (endMonth - startMonth) * 30 + (endDay - startDay);
    }

    public static void main(String[] args) {
        LocalDate settlement = LocalDate.of(2025, 4, 21);
        LocalDate maturity = LocalDate.of(2027, 12, 8);

        BigDecimal couponRate = new BigDecimal("0.06875");
        BigDecimal price = new BigDecimal("94.28");
        BigDecimal redemptionValue = new BigDecimal("100");
        int frequency = 1; // 年付

        MathContext mc = new MathContext(20, RoundingMode.HALF_UP);
        BigDecimal tolerance = new BigDecimal("1E-10");

        BigDecimal yield = calculateYield(settlement, maturity, couponRate, price, redemptionValue, frequency, mc, tolerance, 100);
        System.out.println("计算收益率:" + yield + " %");
        // 输出应接近Excel的9.723637%
    }
}

关键修正说明

  1. 30/360日计数法:严格实现Excel默认的US (NASD) 30/360规则,确保时间计算与Excel一致。
  2. 付息日期推导:正确倒推上一个付息日,计算非完整周期的天数比例。
  3. 全价计算:将输入的净价加上应计利息得到全价,用于折现计算,符合YIELD函数的参数定义。
  4. 精确折现因子:对第一个非完整周期使用指数折现,而非整数周期近似,确保精度。

内容的提问来源于stack exchange,提问作者Cheikh Ibra YADE

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 09:03:11