|
| 1 | +package net.finmath.smartcontract.simulation; |
| 2 | + |
| 3 | +import java.time.LocalDate; |
| 4 | +import java.util.ArrayList; |
| 5 | +import java.util.Arrays; |
| 6 | +import java.util.Comparator; |
| 7 | +import java.util.List; |
| 8 | +import java.util.stream.IntStream; |
| 9 | +import java.util.stream.Stream; |
| 10 | + |
| 11 | +import org.apache.commons.lang3.ArrayUtils; |
| 12 | + |
| 13 | +import net.finmath.marketdata.calibration.CalibratedCurves; |
| 14 | +import net.finmath.marketdata.model.AnalyticModel; |
| 15 | +import net.finmath.marketdata.model.AnalyticModelFromCurvesAndVols; |
| 16 | +import net.finmath.marketdata.model.curves.Curve; |
| 17 | +import net.finmath.marketdata.model.curves.CurveInterpolation; |
| 18 | +import net.finmath.marketdata.model.curves.DiscountCurveInterpolation; |
| 19 | +import net.finmath.marketdata.model.curves.ForwardCurve; |
| 20 | +import net.finmath.marketdata.model.curves.ForwardCurveFromDiscountCurve; |
| 21 | +import net.finmath.marketdata.model.curves.ForwardCurveInterpolation; |
| 22 | +import net.finmath.marketdata.model.curves.ForwardCurveWithFixings; |
| 23 | +import net.finmath.optimizer.SolverException; |
| 24 | +import net.finmath.smartcontract.valuation.marketdata.curvecalibration.CalibrationDataItem; |
| 25 | +import net.finmath.time.FloatingpointDate; |
| 26 | +import net.finmath.time.Period; |
| 27 | +import net.finmath.time.Schedule; |
| 28 | +import net.finmath.time.ScheduleFromPeriods; |
| 29 | +import net.finmath.time.ScheduleGenerator; |
| 30 | +import net.finmath.time.businessdaycalendar.BusinessdayCalendar; |
| 31 | +import net.finmath.time.businessdaycalendar.BusinessdayCalendarExcludingTARGETHolidays; |
| 32 | +import net.finmath.time.daycount.DayCountConvention_ACT_360; |
| 33 | + |
| 34 | + |
| 35 | +public class InterestRateAnalyticCalibration { |
| 36 | + |
| 37 | + public static String DISCOUNT_EUR_OIS = "discount-EUR-OIS"; |
| 38 | + public static String FORWARD_EUR_6M = "forward-EUR-6M"; |
| 39 | + private static String FORWARD_EUR_OIS = "forward-EUR-OIS"; |
| 40 | + private static String FIXED_EUR_6M = "fixed-EUR-6M"; |
| 41 | + |
| 42 | + private List<CalibrationDataItem> fixings; |
| 43 | + |
| 44 | + public InterestRateAnalyticCalibration() { |
| 45 | + this.fixings = new ArrayList<>(); |
| 46 | + } |
| 47 | + |
| 48 | + |
| 49 | + public AnalyticModel getCalibratedModel(LocalDate referenceDate, double[] discountCurveQuotes, double[] forwardCurveQuotes) throws CloneNotSupportedException, SolverException { |
| 50 | + |
| 51 | + final AnalyticModelFromCurvesAndVols model = new AnalyticModelFromCurvesAndVols(new Curve[] { getDiscountCurveEurOIS(referenceDate), getForwardCurveEurOIS(referenceDate), getForwardCurveEur6M(referenceDate)}); |
| 52 | + CalibratedCurves.CalibrationSpec[] specs = |
| 53 | + Stream.of(getCalibrationSpecsEurOIS(referenceDate, discountCurveQuotes), getCalibrationSpecsEur6M(referenceDate, forwardCurveQuotes)) |
| 54 | + .flatMap(Arrays::stream) |
| 55 | + .toArray(CalibratedCurves.CalibrationSpec[]::new); |
| 56 | + |
| 57 | + CalibratedCurves calibratedCurves = new CalibratedCurves(specs, model, 1E-9); |
| 58 | + return calibratedCurves.getModel(); |
| 59 | + } |
| 60 | + |
| 61 | + |
| 62 | + public void addFixingItem(CalibrationDataItem fixingItem) { |
| 63 | + this.fixings.add(fixingItem); |
| 64 | + } |
| 65 | + |
| 66 | + |
| 67 | + private DiscountCurveInterpolation getDiscountCurveEurOIS(LocalDate referenceDate) { |
| 68 | + ArrayList<Double> fixingValuesList = new ArrayList<>(); |
| 69 | + ArrayList<Double> fixingTimesList = new ArrayList<>(); |
| 70 | + ArrayList<Double> dfList = new ArrayList<>(); |
| 71 | + ArrayList<Double> dfTimesList = new ArrayList<>(); |
| 72 | + fixings.stream().filter(x -> x.getCurveName().equals("ESTR")) |
| 73 | + .sorted(Comparator.comparing(CalibrationDataItem::getDate).reversed()) |
| 74 | + .forEach(x -> { |
| 75 | + double time = FloatingpointDate.getFloatingPointDateFromDate( |
| 76 | + referenceDate, |
| 77 | + x.getDate()); |
| 78 | + if (time < 0) { |
| 79 | + fixingTimesList.add(time); |
| 80 | + fixingValuesList.add(365.0 * Math.log(1 + x.getQuote() / 360.0)); |
| 81 | + //conversion from 1-day ESTR (ACT/360) to zero-rate (ACT/ACT) |
| 82 | + //see https://quant.stackexchange.com/questions/73522/how-does-bloomberg-calculate-the-discount-rate-from-eur-estr-curve |
| 83 | + } |
| 84 | + }); |
| 85 | + // Add initial zero entries for calculations |
| 86 | + fixingTimesList.add(0, 0.0); |
| 87 | + fixingValuesList.add(0, 0.0); |
| 88 | + // Add time zero discount factor |
| 89 | + dfTimesList.add(0, 0.0); |
| 90 | + dfList.add(0, 1.0); |
| 91 | + IntStream.range(1, fixingTimesList.size()).forEach(i -> { |
| 92 | + double df = dfList.get(i - 1) * Math.exp(-fixingValuesList.get(i) * (fixingTimesList.get(i) - fixingTimesList.get(i - 1))); |
| 93 | + dfList.add(df); |
| 94 | + dfTimesList.add(fixingTimesList.get(i)); |
| 95 | + }); |
| 96 | + boolean[] isParameters = ArrayUtils.toPrimitive( |
| 97 | + IntStream.range(0, dfTimesList.size()).boxed().map(x -> false).toList().toArray(Boolean[]::new)); |
| 98 | + double[] dfTimes = dfTimesList.stream().mapToDouble(Double::doubleValue).toArray(); |
| 99 | + double[] dfValues = dfList.stream().mapToDouble(Double::doubleValue).toArray(); |
| 100 | + return DiscountCurveInterpolation.createDiscountCurveFromDiscountFactors(DISCOUNT_EUR_OIS, |
| 101 | + referenceDate, dfTimes, dfValues, isParameters, CurveInterpolation.InterpolationMethod.LINEAR, |
| 102 | + CurveInterpolation.ExtrapolationMethod.CONSTANT, CurveInterpolation.InterpolationEntity.LOG_OF_VALUE); |
| 103 | + |
| 104 | + } |
| 105 | + |
| 106 | + private ForwardCurve getForwardCurveEurOIS(LocalDate referenceDate) { |
| 107 | + final ForwardCurve forwardCurve = new ForwardCurveFromDiscountCurve(FORWARD_EUR_OIS, |
| 108 | + DISCOUNT_EUR_OIS, |
| 109 | + DISCOUNT_EUR_OIS, |
| 110 | + referenceDate, |
| 111 | + "1D", |
| 112 | + new BusinessdayCalendarExcludingTARGETHolidays(), |
| 113 | + BusinessdayCalendar.DateRollConvention.FOLLOWING, |
| 114 | + 365.0 / 360.0, 0.0); |
| 115 | + return forwardCurve; |
| 116 | + } |
| 117 | + |
| 118 | + private ForwardCurve getForwardCurveEur6M(LocalDate referenceDate) { |
| 119 | + double[] fixingTimes = fixings.stream().filter(x -> x.getCurveName().equals("Euribor6M")).map(x -> x.getDate()) |
| 120 | + .map(x -> FloatingpointDate.getFloatingPointDateFromDate(referenceDate, x)) |
| 121 | + .mapToDouble(Double::doubleValue).sorted().toArray(); |
| 122 | + if (fixingTimes.length == 0) { //if there are no fixings return empty curve |
| 123 | + return new ForwardCurveInterpolation(FORWARD_EUR_6M, |
| 124 | + referenceDate, |
| 125 | + "6M", |
| 126 | + new BusinessdayCalendarExcludingTARGETHolidays(), |
| 127 | + BusinessdayCalendar.DateRollConvention.FOLLOWING, |
| 128 | + CurveInterpolation.InterpolationMethod.LINEAR, |
| 129 | + CurveInterpolation.ExtrapolationMethod.CONSTANT, |
| 130 | + CurveInterpolation.InterpolationEntity.VALUE, |
| 131 | + ForwardCurveInterpolation.InterpolationEntityForward.FORWARD, |
| 132 | + DISCOUNT_EUR_OIS); |
| 133 | + } |
| 134 | + double[] fixingValues = fixings.stream().filter(x -> x.getCurveName().equals("Euribor6M")) |
| 135 | + .sorted(Comparator.comparing(CalibrationDataItem::getDate)).map(CalibrationDataItem::getQuote) |
| 136 | + .mapToDouble(Double::doubleValue).toArray(); |
| 137 | + ForwardCurve fixedPart = ForwardCurveInterpolation.createForwardCurveFromForwards(FIXED_EUR_6M, |
| 138 | + referenceDate, |
| 139 | + "6M", |
| 140 | + new BusinessdayCalendarExcludingTARGETHolidays(), |
| 141 | + BusinessdayCalendar.DateRollConvention.FOLLOWING, |
| 142 | + CurveInterpolation.InterpolationMethod.LINEAR, |
| 143 | + CurveInterpolation.ExtrapolationMethod.CONSTANT, |
| 144 | + CurveInterpolation.InterpolationEntity.VALUE, |
| 145 | + ForwardCurveInterpolation.InterpolationEntityForward.FORWARD, |
| 146 | + DISCOUNT_EUR_OIS, null, fixingTimes, fixingValues); |
| 147 | + ForwardCurve forwardPart = new ForwardCurveInterpolation(FORWARD_EUR_6M, |
| 148 | + referenceDate, |
| 149 | + "6M", |
| 150 | + new BusinessdayCalendarExcludingTARGETHolidays(), |
| 151 | + BusinessdayCalendar.DateRollConvention.FOLLOWING, |
| 152 | + CurveInterpolation.InterpolationMethod.LINEAR, |
| 153 | + CurveInterpolation.ExtrapolationMethod.CONSTANT, |
| 154 | + CurveInterpolation.InterpolationEntity.VALUE, |
| 155 | + ForwardCurveInterpolation.InterpolationEntityForward.FORWARD, |
| 156 | + DISCOUNT_EUR_OIS); |
| 157 | + // this is a dirty-ish fix: if the extrema of the fixed part lay exactly on the time specified by the data point, |
| 158 | + // some weird jumpiness occurs... TODO: mayb there's a smarter solution |
| 159 | + return new ForwardCurveWithFixings(forwardPart, fixedPart, |
| 160 | + Arrays.stream(fixingTimes).min().orElseThrow() - 1.0 / 365.0, |
| 161 | + Arrays.stream(fixingTimes).max().orElseThrow() + 1.0 / 365.0); |
| 162 | + } |
| 163 | + |
| 164 | + private static CalibratedCurves.CalibrationSpec[] getCalibrationSpecsEurOIS(LocalDate referenceDate, double[] quotes) { |
| 165 | + final String[] maturities = { "1D", "7D", "14D", "21D", "1M", "2M", "3M", "4M", "5M", "6M", "7M", "8M", "9M", "10M", "11M", "1Y", "15M", "18M", "21M", "2Y", "3Y", "4Y", "5Y", "6Y", "7Y", "8Y", "9Y", "10Y", "12Y", "15Y", "20Y", "25Y", "30Y"}; |
| 166 | + final String[] frequency = { "tenor", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual"}; |
| 167 | + |
| 168 | + CalibratedCurves.CalibrationSpec[] specs = new CalibratedCurves.CalibrationSpec[maturities.length]; |
| 169 | + BusinessdayCalendar calendar = new BusinessdayCalendarExcludingTARGETHolidays(); |
| 170 | + // The first product is an overnight cash deposit, followed by 32 swaps |
| 171 | + Schedule scheduleInterfaceRec = ScheduleGenerator.createScheduleFromConventions(referenceDate, 0, "0D", maturities[0], frequency[0], "ACT/360", "first", "follow", calendar, 0, 0); |
| 172 | + double calibrationTime = scheduleInterfaceRec.getPayment(scheduleInterfaceRec.getNumberOfPeriods() - 1); |
| 173 | + specs[0] = new CalibratedCurves.CalibrationSpec(String.format("EUR-OIS-%1$s", maturities[0]), "Deposit", scheduleInterfaceRec, FORWARD_EUR_OIS, quotes[0], DISCOUNT_EUR_OIS, null, "", 0.0, DISCOUNT_EUR_OIS, DISCOUNT_EUR_OIS, calibrationTime); |
| 174 | + |
| 175 | + // OIS ESTR Swaps |
| 176 | + for (int i=1; i < quotes.length; i++) { |
| 177 | + Schedule scheduleRec = ScheduleGenerator.createScheduleFromConventions(referenceDate, 2, "0D", maturities[i], frequency[i], "ACT/360", "first", "modified_following", calendar, 0, 1); |
| 178 | + Schedule schedulePay = ScheduleGenerator.createScheduleFromConventions(referenceDate, 2, "0D", maturities[i], frequency[i], "ACT/360", "first", "modified_following", calendar, 0, 1); |
| 179 | + calibrationTime = scheduleRec.getPayment(scheduleRec.getNumberOfPeriods() - 1); |
| 180 | + specs[i] = new CalibratedCurves.CalibrationSpec(String.format("EUR-OIS-%1$s", maturities[i]), "Swap", scheduleRec, FORWARD_EUR_OIS, 0.0, DISCOUNT_EUR_OIS, schedulePay, "", quotes[i], DISCOUNT_EUR_OIS, DISCOUNT_EUR_OIS, calibrationTime); |
| 181 | + } |
| 182 | + return specs; |
| 183 | + } |
| 184 | + |
| 185 | + private static CalibratedCurves.CalibrationSpec[] getCalibrationSpecsEur6M(LocalDate referenceDate, double[] quotes) { |
| 186 | + final String tenorLabel = "6M"; |
| 187 | + final String[] maturities = { "7M", "8M", "9M", "10M", "12M", "15M", "18M", "2Y", "3Y", "4Y", "5Y", "6Y", "7Y", "8Y", "9Y", "10Y", "12Y", "15Y", "20Y", "25Y", "30Y"}; |
| 188 | + final String[] frequencyFloat = { "", "", "", "", "", "", "", "semiannual", "semiannual", "semiannual", "semiannual", "semiannual", "semiannual", "semiannual", "semiannual", "semiannual", "semiannual", "semiannual", "semiannual", "semiannual", "semiannual"}; |
| 189 | + final String[] frequency = { "tenor", "tenor", "tenor", "tenor", "tenor", "tenor", "tenor", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual", "annual"}; |
| 190 | + final String[] daycountConventionsFloat = { "", "", "", "", "", "", "", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360"}; |
| 191 | + final String[] daycountConventions = { "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "ACT/360", "E30/360", "E30/360", "E30/360", "E30/360", "E30/360", "E30/360", "E30/360", "E30/360", "E30/360", "E30/360", "E30/360", "E30/360", "E30/360", "E30/360"}; |
| 192 | + |
| 193 | + CalibratedCurves.CalibrationSpec[] specs = new CalibratedCurves.CalibrationSpec[maturities.length]; |
| 194 | + BusinessdayCalendar calendar = new BusinessdayCalendarExcludingTARGETHolidays(); |
| 195 | + // The first 7 product are FRAs, followed by 14 swaps |
| 196 | + for (int i=0; i < 8; i++) { |
| 197 | + int nMonthMaturity = Integer.parseInt(maturities[i].replace("M", "")); |
| 198 | + int nMonthOffset = nMonthMaturity - Integer.parseInt(tenorLabel.replace("M", "")); |
| 199 | + String startOffsetLabel = nMonthOffset + "M"; |
| 200 | + |
| 201 | + Schedule scheduleInterfacePrelim = ScheduleGenerator.createScheduleFromConventions(referenceDate, 2, startOffsetLabel, tenorLabel, frequency[i], daycountConventions[i], "first", "follow", calendar, -2, 0); |
| 202 | + LocalDate periodStartDate = scheduleInterfacePrelim.getPeriod(0).getPeriodStart(); |
| 203 | + LocalDate paymentDate = scheduleInterfacePrelim.getPeriod(0).getPayment(); |
| 204 | + LocalDate fixingDate = scheduleInterfacePrelim.getPeriod(0).getFixing(); |
| 205 | + LocalDate periodEndDate = calendar.getAdjustedDate(periodStartDate, tenorLabel, BusinessdayCalendar.DateRollConvention.FOLLOWING); |
| 206 | + |
| 207 | + Period period = new Period(fixingDate, paymentDate, periodStartDate, periodEndDate); |
| 208 | + Schedule scheduleInterfaceFinal = new ScheduleFromPeriods(referenceDate, new DayCountConvention_ACT_360(), period); |
| 209 | + double calibrationTime = scheduleInterfaceFinal.getFixing(scheduleInterfaceFinal.getNumberOfPeriods() - 1); |
| 210 | + // Check correctness of discountCurvePayerName = null |
| 211 | + specs[i] = new CalibratedCurves.CalibrationSpec("EUR-" + tenorLabel + maturities[i], "FRA", scheduleInterfaceFinal, FORWARD_EUR_6M, quotes[i], DISCOUNT_EUR_OIS, null, "", 0.0, null, FORWARD_EUR_6M, calibrationTime); |
| 212 | + } |
| 213 | + // 6M Swaps |
| 214 | + for (int i=8; i < quotes.length; i++) { |
| 215 | + Schedule scheduleInterfaceRec = ScheduleGenerator.createScheduleFromConventions(referenceDate, 2, "0D", maturities[i], frequencyFloat[i], daycountConventionsFloat[i], "first", "modfollow", new BusinessdayCalendarExcludingTARGETHolidays(), -2, 0); |
| 216 | + Schedule scheduleInterfacePay = ScheduleGenerator.createScheduleFromConventions(referenceDate, 2, "0D", maturities[i], frequency[i], daycountConventions[i], "first", "modfollow", new BusinessdayCalendarExcludingTARGETHolidays(), -2, 0); |
| 217 | + double calibrationTime = scheduleInterfaceRec.getFixing(scheduleInterfaceRec.getNumberOfPeriods() - 1); |
| 218 | + specs[i] = new CalibratedCurves.CalibrationSpec("EUR-" + tenorLabel + maturities[i], "Swap", scheduleInterfaceRec, FORWARD_EUR_6M, 0.0, DISCOUNT_EUR_OIS, scheduleInterfacePay, "", quotes[i], DISCOUNT_EUR_OIS, FORWARD_EUR_6M, calibrationTime); |
| 219 | + } |
| 220 | + return specs; |
| 221 | + } |
| 222 | + |
| 223 | + |
| 224 | +} |
0 commit comments