基于Java实现Excel中的yield和price函数

返回定期支付利息的债券的收益。 函数 YIELD 用于计算债券收益率。
毫不客气的说,我可能是全网第一个发这个资源的人吧,因为这个需求我全网找了一个月也没有找到相应代码的Java实现。于是我一跺牙,一咬脚。自己实现了一个版本,并且成功在我司测试上线。准确率无疑是可靠的。唉~

以下描述来自Microsoft 支持

对于输入参数有很多的限制,而且公司不允许把代码复制出来,我在文章里就不写了,具体的限制请到官方支持网站上查看:Excel中的yield函数描述

语法

YIELD(settlement, maturity, rate, pr, redemption, frequency, [basis])
重要: 应使用 DATE 函数输入日期,或者将日期作为其他公式或函数的结果输入。 例如,使用函数 DATE(2008,5,23) 输入 2008 年 5 月 23 日。 如果日期以文本形式输入,则会出现问题。
YIELD 函数语法具有下列参数:

  • Settlement 必需。 有价证券的结算日。 有价证券结算日是在发行日之后,有价证券卖给购买者的日期。
  • Maturity 必需。 有价证券的到期日。 到期日是有价证券有效期截止时的日期。
  • Rate 必需。 有价证券的年息票利率。
  • Pr 必需。 有价证券的价格(按面值为 ¥100 计算)。
  • Redemption 必需。 面值 ¥100 的有价证券的清偿价值。
  • Frequency 必需。 年付息次数。 如果按年支付,frequency = 1;按半年期支付,frequency = 2;按季支付,frequency = 4。Basis 可选。 要使用的日计数基准类型。
  • Basis 日计数基准

计算公式部分

第一个需要注意的:如果在清偿日之前只有一个或是没有付息期,函数yield的计算公式为:

java 引用Excel的计算函数 java调用excel函数_java


如果付息期不止有一个那么则通过 100 次迭代来计算函数 YIELD。 基于函数 PRICE 中给出的公式,并使用牛顿迭代法不断修正计算结果。 这样,收益率将不断更改,直到根据给定收益率计算的估计价格接近实际价格。

也就是说想要求解yield先要实现prices函数:

PRICE (settlement, maturity, rate, yld, redemption, frequency, [basis])

函数计算支付定期利息的证券每面值 100 美元的价格。

java 引用Excel的计算函数 java调用excel函数_金融_02

具体参数描述同yield一样的,也可以在微软支持页面找到这个函数的描述,其中有几个特别迷惑的中间变量:
N 是结算日与清偿日之间的付息次数),
DSC 结算日与下一付息日之间的天数。
E 结算日所在的付息期的天数。
A 当前付息期内截止到结算日的天数。
这四个数到底什么意思,是我这次开发过程中最痛苦的事情,在我不懈的努力,近乎与穷举法的尝试下,我发现原来这四个参数对应Excel中的其他四个函数,具体是哪几个有需要的话可以在评论区评论,我会一一对应的发给你的(还是那句话,公司不让带文件出来,很多东西抄写出来真的不方便)。

首先是price函数
public class YtmUtil {
    private static final int YEARS = 360;

    private static final int MONTHS = 12;

    public static double prices(LocalDate settlement, LocalDate maturity, double rate, double yld, double redemption, int frequency) {
        int interval = MONTHS/frequency;
        int E = YEARS/frequency;

        double N = 0;
        while (settlement.isBefore(maturity)) {
            settlement = settlement.plusMonths(interval);
            N++;
        }

        int settlementMonth = settlement.getMonthValue()%interval;
        int maturityMonth = maturity.getMonthValue()%interval;
        int A = (settlementMonth - maturityMonth)*30 + (settlement.getDayOfMonth() - maturity.getDayOfMonth());
        if (A < 0) {
            A += YEARS/frequency;
        }
        int DSC = E - A;
        if (N == 1) {
            return prices(settlement, maturity, rate, yld, redemption, DSC, E, A, frequency);
        }
        return prices(settlement, maturity, rate, yld, redemption, DSC, E, A, N, frequency);
    }

    static double prices(LocalDate settlement, LocalDate maturity, double rate, double yld, double redemption,
                         double DSC, double E, double A, double N, long frequency) {
        double g1 = redemption/(Math.pow(1+yld/frequency, N - 1 + (DSC/E)) );
        double g2 = 0;
        for (int k = 1; k <= N; k++) {
            g2 += (100*rate/frequency)/Math.pow(1 + yld/frequency, k - 1 + (DSC/E));
        }
        double g3 = (100*(rate/frequency)*(A/E));
        return g1 + g2 - g3;
    }

    static double prices(LocalDate settlement, LocalDate maturity, double rate, double yld, double redemption,
                         double DSC, double E, double A, long frequency) {
        double DSR = E - A;
        double T1 = 100*(rate/frequency) + redemption;
        double T2 = (yld/frequency)*(DSR/E) + 1;
        double T3 = 100*(rate/frequency)*(A/E);
        return T1/T2 - T3;
    }
}

在微软支持网站上写的是yield函数是通过牛顿迭代法计算出来的,我认为这是官方在吹牛B,大家都知道牛顿迭代法需要求导,这么复杂的公式导出来的结果也是很复杂的,不知道是不是我数学太菜了。总之我使用牛顿迭代法求解丢失精度很严重,求出来的结果都是0。

在这里我使用的是二分查找求解
public static double yield(LocalDate settlement, LocalDate maturity, double rate, double pr, double redemption, int frequency) {
        double left = 0, right = 1, yld = 0.5;
        double proPr = prices(settlement, maturity, rate, yld, redemption, frequency);
        while (Math.abs(pr - proPr) > precision){
            if (proPr > pr) {
                right = yld;
            }else {
                left = yld;
            }
            yld = (left + right)/2;
            proPr = prices(settlement, maturity, rate, yld, redemption, frequency);
        }
        return yld;
    }

以上代码是我凭借记忆写出来的,而且有不足的地方,比如yield的值完全有可能大于1,此时根据你的需要把二分法改进一下就好了。
应该是全网首发,谢谢!!!

///------------------------------------
后续更新,有不少人看了这个文章私信我问一些细节问题,这说明从事金融行业的兄弟们对于计算债券收益率的这个需求还是有的而且还挺麻烦的。我本来打算把代码传到github上,兄弟们看着也方便。但是房东给办的网犯病,死活push不上去。我先把代码放在这了,写上了注释,basis也加上了,自己定义一个FinancialException就能用了,与君共勉!

package com.psyche.financial.utils;

import com.psyche.financial.exception.FinancialException;
import lombok.Data;
import lombok.val;

import java.time.LocalDate;
import java.time.Month;
import java.time.temporal.ChronoUnit;

/**
 * @program: fin-demo
 * @description: ytm计算
 * @author: Lxl
 * @create: 2024-01-21 14:27
 **/
@Data
public class YTM {
    private static final double YEAR = 360;

    private static final long MONTHS = 12;

    private static final double PRECISE = 0.0000001;
    private static final int TRY_TIME = 50;

    /**
     * @param [settlement, maturity, rate, price, redemption, frequency, basis]
     * @return double
     * @throws
     * @description: 计算有价证券的收益率。
     * @author Lxl
     * @date 2024/1/21 下午10:19
     */
    public static double yield(LocalDate settlement, LocalDate maturity, double rate, double price, double redemption, double frequency, int basis) throws FinancialException {
        double initValue = YTM.price(settlement, maturity, rate, 2, redemption, frequency, basis);
        int count = 0;
        if (initValue < price) {
            initValue = YTM.price(settlement, maturity, rate, 2, redemption, frequency, basis);
            while (initValue < price && count < TRY_TIME) {
                initValue = YTM.price(settlement, maturity, rate, 2 * (count++), redemption, frequency, basis);
            }
            return YTM.yield(settlement, maturity, rate, price, redemption, frequency, basis, 2 * (count - 1), 2 * count);
        } else if (!(YTM.price(settlement, maturity, rate, -2, redemption, frequency, basis) < price)) {
            return YTM.yield(settlement, maturity, rate, price, redemption, frequency, basis, -2, 2);
        } else {
            initValue = YTM.price(settlement, maturity, rate, -2, redemption, frequency, basis);
            while (initValue > price && count < TRY_TIME) {
                initValue = YTM.price(settlement, maturity, rate, -2 * (count++), redemption, frequency, basis);
            }
            return YTM.yield(settlement, maturity, rate, price, redemption, frequency, basis, -2 * count, -2 * (count - 1));
        }
    }

    private static double yield(LocalDate settlement, LocalDate maturity, double rate, double price, double redemption, double frequency, int basis, double left, double right) throws FinancialException {
        double yld = 0;
        int count = 0;
        while (Math.abs(price(settlement, maturity, rate, yld, redemption, frequency, basis) - price) > PRECISE && count++ < 100) {
            double ePr = price(settlement, maturity, rate, yld, redemption, frequency, basis);
            if (ePr < price) {
                right = yld;
            } else {
                left = yld;
            }
            yld = (right + left) / 2;
        }
        return yld;
    }

    /**
     * @param [Settlement, Maturity, Rate, Yield, Redemption, Frequency, basis]
     * @return double
     * @throws
     * @description: 根据目标收益率计算每股 100 货币单位的定额利息有价证券的市值,预测该有价证券的收益率。
     * 「Settlement」指有价证券的购买日期。
     * 「Maturity」该有价证券的到期日 (期满)。
     * 「Rate」指年名义利率 (息票利率)
     * 「Yield」有价证券的年收益率。
     * 「Redemption」是面额为 100 个货币单位的清偿价格。
     * 「Frequency」指每年支付利息的次数 (1、2 或 4)。
     * 「Basis」是从选项列表中选择的,并指明计算年的方式。
     * @author Lxl
     * @date 2024/1/21 下午2:43
     */
    public static double price(LocalDate settlement, LocalDate maturity, double rate, double yield, double redemption, double frequency, Integer basis) throws FinancialException {
        if (yield < 0 || rate < 0) {
            throw new FinancialException("#NUM! Yield and rate must be positive");
        }
        if (frequency != 1 && frequency != 2 && frequency != 4) {
            throw new FinancialException("#NUM!");
        }
        if (basis < 0 || basis > 4) {
            throw new FinancialException("#NUM!");
        }
        if (settlement.isAfter(maturity)) {
            throw new FinancialException("#NUM! Settlement date must be on or before maturity date");
        }
        double A = coupDayBs(settlement, maturity, frequency, basis);
        double E = coupDays(settlement, maturity, frequency, basis);
        double N = coupNum(settlement, maturity, frequency, basis);
        double DSC = coupDaySNC(settlement, maturity, frequency, basis);
        if (N == 1) {
            return price(A, E, rate, yield, redemption, frequency);
        } else {
            return price(A, E, N, DSC, rate, yield, redemption, frequency);
        }
    }

    /**
     * @param settlement, maturity, rate, yield, redemption, frequency
     * @return double
     * @throws
     * @description: 根据目标收益率计算每股 100 货币单位的定额利息有价证券的市值,预测该有价证券的收益率。 其中basis = 0
     * @author Lxl
     * @date 2024/1/21 下午3:00
     */
    public static double price(LocalDate settlement, LocalDate maturity, double rate, double yield, double redemption, double frequency) throws FinancialException {
        return price(settlement, maturity, rate, yield, redemption, frequency, 0);
    }

    private static double price(double A, double E, double N, double DSC, double rate, double yld, double redemption, double frequency) {
        double g1 = redemption / Math.pow(1 + yld / frequency, N - 1 + DSC / E);
        double g2 = 0;
        for (int k = 1; k <= N; k++) {
            g2 += (100 * rate / frequency) / Math.pow(1 + yld / frequency, (k - 1) + DSC / E);
        }
        double g3 = (100 * (rate / frequency) * (A / E));
        return g1 + g2 - g3;
    }

    private static double price(double A, double E, double rate, double yld, double redemption, double frequency) {
        double DSR = E - A;
        double t1 = 100 * rate / frequency + redemption;
        double t2 = (yld / frequency) * (DSR / E) + 1;
        double t3 = 100 * rate / frequency * (A / E);
        return t1 / t2 - t3;
    }

    /**
     * @param [settlement, maturity, frequency, basis]
     * @return double
     * @throws
     * @description: 返回从结算日期起至到期日期的付息 (利息支付) 次数。
     * @author Lxl
     * @date 2024/1/21 下午3:54
     */
    public static double coupNum(LocalDate settlement, LocalDate maturity, double frequency, int basis) {
        long interval = (long) (MONTHS / frequency);
        LocalDate start = settlement.plusDays(0);
        int count = 0;
        while (start.isBefore(maturity)) {
            start = start.plusMonths(interval);
            count++;
        }
        return count;
    }

    /**
     * @param [settlement, maturity, frequency, basis]
     * @return double
     * @throws
     * @description: 计算当前计息期间 (结算日期所在期间) 的天数。
     * @author Lxl
     * @date 2024/1/21 下午4:11
     */
    public static double coupDays(LocalDate settlement, LocalDate maturity, double frequency, int basis) {
        long interval = (long) (MONTHS / frequency);
        return switch (basis) {
            case 0, 4 -> interval * 30;
            case 1, 2 -> {
                LocalDate payDay = settlement.plusMonths(interval);
                yield ChronoUnit.DAYS.between(settlement, payDay);
            }
            default -> throw new IllegalStateException("Unexpected value: " + basis);
        };
    }

    /**
     * @param [settlement, maturity, frequency, basis]
     * @return double
     * @throws
     * @description: 返回某一有价证券从首期付息日期起到结算日期的天数。
     * @author Lxl
     * @date 2024/1/21 下午8:27
     */
    public static double coupDayBs(LocalDate settlement, LocalDate maturity, double frequency, int basis) {
        double interval, settlementMonth, maturityMonth;
        interval = MONTHS / frequency;
        settlementMonth = settlement.getMonthValue() % interval;
        maturityMonth = maturity.getMonthValue() % interval;

        double A = (settlementMonth - maturityMonth) * 30 + (settlement.getDayOfMonth() - maturity.getDayOfMonth());
        if (A < 0) {
            A += switch (basis) {
                case 0, 4 -> YEAR / frequency;
                case 1 -> settlement.lengthOfYear() / frequency;
                case 3 -> 365 / frequency;
                default -> throw new IllegalStateException("Unexpected value: " + basis);
            };
        }
        return A;
    }

    /**
     * @param [settlement, maturity, frequency, basis]
     * @return double
     * @throws
     * @description: 返回从结算日期起到下一个付息日期为止的天数。
     * @author Lxl
     * @date 2024/1/21 下午8:43
     */
    public static double coupDaySNC(LocalDate settlement, LocalDate maturity, double frequency, int basis) {
        double E = coupDays(settlement, maturity, frequency, basis);
        double A = coupDayBs(settlement, maturity, frequency, basis);
        return E - A;
    }

}

今天大年初一回家了,拜托了房东那若有若无的网络终于把项目上传了,欢迎各位金融同僚的批评指正:项目github代码地址