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函数的精确时间计算规则,具体包括:
- 日计数基准错误:Excel YIELD默认使用US (NASD) 30/360日计数法,而非实际天数/365。
- 未处理非完整付息周期:原代码直接将总天数折算为整数个付息周期,忽略了结算日到下一个付息日的非完整周期,以及应计利息的影响。
- 全价/净价混淆:Excel YIELD的
price参数是净价,需要加上应计利息得到全价后再进行折现计算,原代码直接用净价参与折现。 - 付息日期计算错误:未正确推导上一个付息日、下一个付息日,导致周期计算完全偏离YIELD的逻辑。
修正方案
要实现与Excel一致的YIELD计算,必须严格遵循以下步骤:
- 计算付息日期序列:根据到期日和付息频率,倒推上一个付息日(PCD)、下一个付息日(NCD),以及后续所有付息日。
- 应用30/360日计数法:计算PCD到结算日的天数(A)、PCD到NCD的天数(E)、结算日到NCD的天数(DSC)、PCD到期日的总天数(T)。
- 计算应计利息:
accruedInterest = (票面利率/付息频率) * (A/E)。 - 调整为全价:
fullPrice = price + accruedInterest。 - 迭代求解收益率:使用牛顿-拉夫逊法,基于全价和精确的折现周期(包括第一个非完整周期)计算收益率。
修正后的代码
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% } }
关键修正说明
- 30/360日计数法:严格实现Excel默认的US (NASD) 30/360规则,确保时间计算与Excel一致。
- 付息日期推导:正确倒推上一个付息日,计算非完整周期的天数比例。
- 全价计算:将输入的净价加上应计利息得到全价,用于折现计算,符合YIELD函数的参数定义。
- 精确折现因子:对第一个非完整周期使用指数折现,而非整数周期近似,确保精度。
内容的提问来源于stack exchange,提问作者Cheikh Ibra YADE
相关产品推荐
相关产品推荐

