Skip to content

Commit 82082e6

Browse files
author
Raphael Prandtl
committed
Small improvements and test cases for refinement of the valuation oracle
1 parent 0dbeef3 commit 82082e6

File tree

12 files changed

+1208
-93
lines changed

12 files changed

+1208
-93
lines changed

src/main/java/net/finmath/smartcontract/product/flowschedule/FlowScheduleSwapLegPeriod.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"paymentDate",
2525
"notional",
2626
"rate",
27-
"flowAmount"
27+
"flow",
28+
"npv"
2829
})
2930
public class FlowScheduleSwapLegPeriod {
3031
LocalDateTime fixingDate;
@@ -33,7 +34,8 @@ public class FlowScheduleSwapLegPeriod {
3334
LocalDateTime paymentDate;
3435
double notional;
3536
double rate;
36-
double flowAmount;
37+
double flow;
38+
double npv;
3739

3840
@XmlElement(name = "fixingDate")
3941
@XmlJavaTypeAdapter(LocalDateTimeAdapter.class)
@@ -93,15 +95,22 @@ public void setRate(final double rate) {
9395
this.rate = rate;
9496
}
9597

96-
@XmlElement(name = "flowAmount")
97-
public double getFlowAmount() {
98-
return flowAmount;
98+
@XmlElement(name = "flow")
99+
public double getFlow() {
100+
return flow;
99101
}
100102

101-
public void setFlowAmount(final double flowAmount) {
102-
this.flowAmount = flowAmount;
103+
public void setFlow(final double flow) {
104+
this.flow = flow;
103105
}
104106

107+
@XmlElement(name = "npv")
108+
public double getNpv() {
109+
return npv;
110+
}
105111

112+
public void setNpv(final double npv) {
113+
this.npv = npv;
114+
}
106115

107116
}

src/main/java/net/finmath/smartcontract/valuation/implementation/FlowScheduleCalculator.java

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import net.finmath.marketdata.model.AnalyticModel;
44
import net.finmath.marketdata.model.curves.DiscountCurve;
5+
import net.finmath.marketdata.model.curves.ForwardCurve;
56
import net.finmath.marketdata.products.AnalyticProduct;
67
import net.finmath.modelling.descriptor.InterestRateSwapLegProductDescriptor;
78
import net.finmath.modelling.descriptor.InterestRateSwapProductDescriptor;
@@ -165,20 +166,27 @@ private List<FlowScheduleSwapLegPeriod> getFlowScheduleSwapLegPeriods(InterestRa
165166
InterestRateSwapLegProductDescriptor singlePeriodSwapLeg = new InterestRateSwapLegProductDescriptor(forwardCurveName, discountCurveName, scheduleDescriptor, notionals[i], spreads[i], isNotionalExchanged);
166167
AnalyticProduct product = (AnalyticProduct) productFactory.getProductFromDescriptor(singlePeriodSwapLeg);
167168
DiscountCurve discountCurve = model.getDiscountCurve(discountCurveName);
168-
169-
double flowAmount = product.getValue(0.0, model); // Flow schedule is always calculated as of marketDataTime = 0.0
170-
double periodLength = schedule.getPeriodLength(0); // always periodIndex 0 for single period swap leg
169+
ForwardCurve forwardCurve = model.getForwardCurve(forwardCurveName);
170+
double npv = product.getValue(0.0, model); // Flow schedule is always calculated as of marketDataTime = 0.0
171171
double discountFactor = discountCurve.getDiscountFactor(scheduleFromPeriods.getPayment(i) + productToModelTimeOffset); // payment time relative to marketDataTime
172-
double rate = flowAmount / discountFactor / periodLength / notionals[i]; // "reverse" swap leg getValue() for each period
172+
double periodLength = schedule.getPeriodLength(0); // always periodIndex 0 for single period swap leg
173+
double rate;
174+
if (forwardCurve != null) {
175+
rate = forwardCurve.getForward(model, scheduleFromPeriods.getFixing(i) + productToModelTimeOffset);
176+
} else {
177+
rate = npv / discountFactor / periodLength / notionals[i]; // "reverse" swap leg getValue() for each period
178+
}
179+
double flow = notionals[i] * periodLength * rate;
173180
if (legType.equals(LegType.LEG_PAYER)) {
174-
flowAmount = (-1.0) * flowAmount;
181+
flow = (-1.0) * flow;
182+
npv = (-1.0) * npv;
175183
}
176-
flowScheduleSwapLegPeriods.add(createFlowScheduleSwapLegPeriod(period, notionals[i], flowAmount, rate));
184+
flowScheduleSwapLegPeriods.add(createFlowScheduleSwapLegPeriod(period, notionals[i], rate, flow, npv));
177185
}
178186
return flowScheduleSwapLegPeriods;
179187
}
180188

181-
private FlowScheduleSwapLegPeriod createFlowScheduleSwapLegPeriod(Period period, double notional, double flowAmount, double rate) {
189+
private FlowScheduleSwapLegPeriod createFlowScheduleSwapLegPeriod(Period period, double notional, double rate, double flow, double npv) {
182190
FlowScheduleSwapLegPeriod flowScheduleSwapLegPeriod = new FlowScheduleSwapLegPeriod();
183191
// TODO Replace atStartOfDay with correct settlementTime
184192
flowScheduleSwapLegPeriod.setFixingDate(period.getFixing().atStartOfDay());
@@ -187,8 +195,8 @@ private FlowScheduleSwapLegPeriod createFlowScheduleSwapLegPeriod(Period period,
187195
flowScheduleSwapLegPeriod.setPaymentDate(period.getPayment().atStartOfDay());
188196
flowScheduleSwapLegPeriod.setNotional(notional);
189197
flowScheduleSwapLegPeriod.setRate(rate);
190-
flowScheduleSwapLegPeriod.setFlowAmount(flowAmount);
191-
198+
flowScheduleSwapLegPeriod.setFlow(flow);
199+
flowScheduleSwapLegPeriod.setNpv(npv);
192200
return flowScheduleSwapLegPeriod;
193201
}
194202

@@ -212,8 +220,6 @@ private AnalyticModel getCalibratedModel(String productData, String marketData)
212220
private AnalyticModel calibrateModel(String productData, String marketData) {
213221
SmartDerivativeContractDescriptor productDescriptor = getSmartderivativeContractDescriptor(productData);
214222
CalibrationDataset calibrationDataset = CalibrationParserDataItems.getCalibrationDataSetFromXML(marketData, productDescriptor.getMarketdataItemList());
215-
// Add most recent published overnight rate as proxy for discounting from t=0 to t+1
216-
addOvernightDepositRate(calibrationDataset);
217223
LocalDateTime marketDataTime = calibrationDataset.getDate();
218224
final CalibrationParserDataItems parser = new CalibrationParserDataItems();
219225
try {
@@ -231,27 +237,6 @@ private AnalyticModel calibrateModel(String productData, String marketData) {
231237
}
232238
}
233239

234-
// Search for the nearest ESTR fixing and add it to the calibration items as 1-day discount rate proxy
235-
private void addOvernightDepositRate(CalibrationDataset calibrationDataset) {
236-
List<CalibrationDataItem> estrFixings = calibrationDataset.getFixingDataItems().stream().filter(fixingItem ->
237-
fixingItem.getSpec().getCurveName().equals("ESTR") &&
238-
fixingItem.getSpec().getMaturity().equals("1D")).toList();
239-
if (!estrFixings.isEmpty()) {
240-
CalibrationDataItem nearestFixing = estrFixings.stream()
241-
.min((item1, item2) -> {
242-
double diff1 = FloatingpointDate.getFloatingPointDateFromDate(calibrationDataset.getDate(), item1.getDateTime());
243-
double diff2 = FloatingpointDate.getFloatingPointDateFromDate(calibrationDataset.getDate(), item2.getDateTime());
244-
return Double.compare(Math.abs(diff1), Math.abs(diff2));
245-
})
246-
.orElse(null);
247-
if (nearestFixing != null) {
248-
CalibrationDataItem.Spec calibrationItemSpecON = new CalibrationDataItem.Spec("EUREST1D", "ESTR","Overnight-Rate","1D");
249-
CalibrationDataItem calibrationItemON = new CalibrationDataItem(calibrationItemSpecON, nearestFixing.getQuote(), nearestFixing.getDateTime());
250-
calibrationDataset.getCalibrationDataItems().add(calibrationItemON);
251-
}
252-
}
253-
}
254-
255240
private InterestRateSwapLegProductDescriptor getInterestRateSwapLegProductDescriptor(String productData, LegType legType) {
256241
SmartDerivativeContractDescriptor productDescriptor = getSmartderivativeContractDescriptor(productData);
257242
String ownerPartyID = productDescriptor.getUnderlyingReceiverPartyID();

src/main/java/net/finmath/smartcontract/valuation/marketdata/curvecalibration/CalibrationParserDataItems.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ private Optional<CalibrationSpecProvider> parseDatapointIfPresent(final Calibrat
3939
case "ESTR", "EONIA" -> {
4040
if (datapoint.getProductName().equals("Swap-Rate"))
4141
return Optional.of(new CalibrationSpecProviderOis(datapoint.getMaturity(), "annual", datapoint.getQuote()));
42-
if (datapoint.getProductName().equals("Overnight-Rate"))
43-
return Optional.of(new CalibrationSpecProviderON(datapoint.getMaturity(), "tenor", datapoint.getQuote()));
42+
if (datapoint.getProductName().equals("Deposit") && datapoint.getMaturity().equals("1D")) {
43+
return Optional.of(new CalibrationSpecProviderOvernightRate(datapoint.getMaturity(), "tenor", datapoint.getQuote()));
44+
}
4445
else
4546
return Optional.empty();
4647
}

src/main/java/net/finmath/smartcontract/valuation/marketdata/curvecalibration/CalibrationSpecProviderFRA.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22

33

44
import net.finmath.marketdata.calibration.CalibratedCurves;
5+
import net.finmath.time.Period;
56
import net.finmath.time.Schedule;
7+
import net.finmath.time.ScheduleFromPeriods;
68
import net.finmath.time.ScheduleGenerator;
9+
import net.finmath.time.businessdaycalendar.BusinessdayCalendar;
710
import net.finmath.time.businessdaycalendar.BusinessdayCalendarExcludingTARGETHolidays;
11+
import net.finmath.time.daycount.DayCountConvention_ACT_360;
12+
13+
import java.time.LocalDate;
814

915
/**
1016
* A calibration spec provider for fras.
@@ -36,11 +42,23 @@ public CalibrationSpecProviderFRA(final String tenorLabel, final String maturity
3642

3743
@Override
3844
public CalibratedCurves.CalibrationSpec getCalibrationSpec(final CalibrationContext ctx) {
39-
final Schedule scheduleInterfaceRec = ScheduleGenerator.createScheduleFromConventions(ctx.getReferenceDate(), 2, startOffsetLabel, tenorLabel, "tenor", "ACT/360", "first", "modfollow", new BusinessdayCalendarExcludingTARGETHolidays(), -2, 0);
40-
final double calibrationTime = scheduleInterfaceRec.getFixing(scheduleInterfaceRec.getNumberOfPeriods() - 1);
45+
BusinessdayCalendar calendar = new BusinessdayCalendarExcludingTARGETHolidays();
46+
final Schedule scheduleInterfacePrelim = ScheduleGenerator.createScheduleFromConventions(ctx.getReferenceDate(), 2, startOffsetLabel, tenorLabel, "tenor", "ACT/360", "first", "follow", calendar, -2, 0);
47+
// periodStartDate = referenceDate + spotOffsetDays (2BD) + startOffsetLabel (e.g. 2M) + dateRollConvention (follow)
48+
// fixingDate = periodStartDate + fixingOffset (-2BD)
49+
// period end = period start + tenorLabel (6M) -> this roll out is currently not supported by createScheduleFromConventions
50+
// paymentDate = periodStartDate BUT According to Ametrano/ Bianchetti (2013) p.22, the size of the convexity adjustment between market FRA and textbook FRA is negligible
51+
// We leave the payment date as it is, and are discounting the FRA legs by the forward rate
52+
LocalDate periodStartDate = scheduleInterfacePrelim.getPeriod(0).getPeriodStart();
53+
LocalDate paymentDate = scheduleInterfacePrelim.getPeriod(0).getPayment();
54+
LocalDate fixingDate = scheduleInterfacePrelim.getPeriod(0).getFixing();
55+
LocalDate periodEndDate = calendar.getAdjustedDate(periodStartDate, tenorLabel, BusinessdayCalendar.DateRollConvention.FOLLOWING);
4156

42-
final String curveName = String.format("forward-EUR-%1$s", tenorLabel);
57+
Period period = new Period(fixingDate, paymentDate, periodStartDate, periodEndDate);
58+
final Schedule scheduleInterfaceFinal = new ScheduleFromPeriods(ctx.getReferenceDate(), new DayCountConvention_ACT_360(), period);
59+
final double calibrationTime = scheduleInterfaceFinal.getFixing(scheduleInterfaceFinal.getNumberOfPeriods() - 1);
4360

44-
return new CalibratedCurves.CalibrationSpec("EUR-" + tenorLabel + maturityLabel, "FRA", scheduleInterfaceRec, curveName, fraRate, "discount-EUR-OIS", null, "", 0.0, null, curveName, calibrationTime);
61+
final String curveName = String.format("forward-EUR-%1$s", tenorLabel);
62+
return new CalibratedCurves.CalibrationSpec("EUR-" + tenorLabel + maturityLabel, "FRA", scheduleInterfaceFinal, curveName, fraRate, "discount-EUR-OIS", null, "", 0.0, null, curveName, calibrationTime);
4563
}
4664
}

src/main/java/net/finmath/smartcontract/valuation/marketdata/curvecalibration/CalibrationSpecProviderON.java renamed to src/main/java/net/finmath/smartcontract/valuation/marketdata/curvecalibration/CalibrationSpecProviderOvernightRate.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
*
1818
* @author Raphael Prandtl
1919
*/
20-
public class CalibrationSpecProviderON implements CalibrationSpecProvider {
20+
public class CalibrationSpecProviderOvernightRate implements CalibrationSpecProvider {
2121
private final String maturityLabel;
2222
private final String frequency;
2323
private final double overnightRate;
2424

25-
public CalibrationSpecProviderON(final String maturityLabel, final String frequency, final double overnightRate) {
25+
public CalibrationSpecProviderOvernightRate(final String maturityLabel, final String frequency, final double overnightRate) {
2626
this.maturityLabel = maturityLabel;
2727
this.frequency = frequency;
2828
this.overnightRate = overnightRate;

src/main/java/net/finmath/smartcontract/valuation/marketdata/curvecalibration/Calibrator.java

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import net.finmath.time.businessdaycalendar.BusinessdayCalendarExcludingTARGETHolidays;
1010
import org.apache.commons.lang3.ArrayUtils;
1111

12-
import java.time.LocalDateTime;
12+
import java.time.LocalDate;
1313
import java.util.*;
1414
import java.util.stream.IntStream;
1515
import java.util.stream.Stream;
@@ -27,12 +27,12 @@ public class Calibrator {
2727
public static final String FORWARD_EUR_6M = "forward-EUR-6M";
2828

2929
private final List<CalibrationDataItem> fixings;
30-
private final LocalDateTime referenceDateTime;
30+
private final LocalDate referenceDate;
3131
private CalibratedCurves calibratedCurves;
3232

3333
public Calibrator(List<CalibrationDataItem> fixings, CalibrationContext ctx) {
3434
this.fixings = fixings;
35-
this.referenceDateTime = ctx.getReferenceDateTime();
35+
this.referenceDate = ctx.getReferenceDate();
3636
}
3737

3838
protected CalibratedCurves getCalibratedCurves() {
@@ -51,6 +51,10 @@ public Optional<CalibrationResult> calibrateModel(final Stream<CalibrationSpecPr
5151
final AnalyticModelFromCurvesAndVols model = new AnalyticModelFromCurvesAndVols(getCalibrationCurves(ctx));
5252
CalibratedCurves.CalibrationSpec[] specs =
5353
providers.map(c -> c.getCalibrationSpec(ctx)).toArray(CalibratedCurves.CalibrationSpec[]::new);
54+
// Add most recent published overnight rate as proxy for discounting from t=0 to t+1
55+
Optional<CalibratedCurves.CalibrationSpec> calibrationSpecOvernightRate = getCalibrationSpecOvernightRate(ctx);
56+
specs = Stream.concat(calibrationSpecOvernightRate.stream(), // empty stream if the optional is empty
57+
Arrays.stream(specs)).toArray(CalibratedCurves.CalibrationSpec[]::new);
5458

5559
try {
5660
calibratedCurves = new CalibratedCurves(specs, model, ctx.getAccuracy());
@@ -66,9 +70,9 @@ private Curve[] getCalibrationCurves(final CalibrationContext ctx) {
6670
}
6771

6872
/*
69-
We build the curve w.r.t to the reference Date AND Time, i.e. the referenceDateTime represents the time point 0.0 of the curve
70-
Only the fixing dates of the historical fixings are relevant, i.e. we ignore the fixing time within a day and measure the previous fixings relative to the referenceDateTime in whole days (no day fractions)
71-
The calibration items and the swap schedule use LocalDate exclusively, i.e. the curve points and schedule dates are also measured in whole days relative to the referenceDateTime / time 0.0
73+
We build the curve w.r.t to the reference Date, i.e. the referenceDate represents the time point 0.0 of the curve
74+
Only the fixing dates of the historical fixings are relevant, i.e. we ignore the fixing time within a day and measure the previous fixings relative to the referenceDate in whole days (no intraday fractions)
75+
The calibration items and the swap schedule use LocalDate exclusively, i.e. the curve points and schedule dates are also measured in whole days relative to the referenceDate/time 0
7276
*/
7377
private DiscountCurveInterpolation getOisDiscountCurve(final CalibrationContext ctx) {
7478
ArrayList<Double> fixingValuesList = new ArrayList<>();
@@ -79,8 +83,8 @@ private DiscountCurveInterpolation getOisDiscountCurve(final CalibrationContext
7983
.sorted(Comparator.comparing(CalibrationDataItem::getDate).reversed())
8084
.forEach(x -> {
8185
double time = FloatingpointDate.getFloatingPointDateFromDate(
82-
referenceDateTime,
83-
x.dateTime);
86+
referenceDate,
87+
x.dateTime.toLocalDate());
8488
if (time < 0) {
8589
fixingTimesList.add(time);
8690
fixingValuesList.add(365.0 * Math.log(1 + x.quote / 360.0));
@@ -106,7 +110,6 @@ private DiscountCurveInterpolation getOisDiscountCurve(final CalibrationContext
106110
return DiscountCurveInterpolation.createDiscountCurveFromDiscountFactors(DISCOUNT_EUR_OIS,
107111
ctx.getReferenceDate(), dfTimes, dfValues, isParameters, CurveInterpolation.InterpolationMethod.LINEAR,
108112
CurveInterpolation.ExtrapolationMethod.CONSTANT, CurveInterpolation.InterpolationEntity.LOG_OF_VALUE);
109-
110113
}
111114

112115
private ForwardCurve getOisForwardCurve(final CalibrationContext ctx) {
@@ -121,8 +124,8 @@ private ForwardCurve getOisForwardCurve(final CalibrationContext ctx) {
121124
}
122125

123126
private ForwardCurve get3MForwardCurve(final CalibrationContext ctx) {
124-
double[] fixingTimes = fixings.stream().filter(x -> x.getCurveName().equals("Euribor3M")).map(x -> x.dateTime)
125-
.map(x -> FloatingpointDate.getFloatingPointDateFromDate(referenceDateTime, x))
127+
double[] fixingTimes = fixings.stream().filter(x -> x.getCurveName().equals("Euribor3M")).map(x -> x.dateTime.toLocalDate())
128+
.map(x -> FloatingpointDate.getFloatingPointDateFromDate(referenceDate, x))
126129
.mapToDouble(Double::doubleValue).sorted().toArray();
127130
if (fixingTimes.length == 0) { //if there are no fixings return empty curve
128131
return new ForwardCurveInterpolation("forward-EUR-3M",
@@ -167,8 +170,8 @@ private ForwardCurve get3MForwardCurve(final CalibrationContext ctx) {
167170
}
168171

169172
private ForwardCurve get6MForwardCurve(final CalibrationContext ctx) {
170-
double[] fixingTimes = fixings.stream().filter(x -> x.getCurveName().equals("Euribor6M")).map(x -> x.dateTime)
171-
.map(x -> FloatingpointDate.getFloatingPointDateFromDate(referenceDateTime, x))
173+
double[] fixingTimes = fixings.stream().filter(x -> x.getCurveName().equals("Euribor6M")).map(x -> x.dateTime.toLocalDate())
174+
.map(x -> FloatingpointDate.getFloatingPointDateFromDate(referenceDate, x))
172175
.mapToDouble(Double::doubleValue).sorted().toArray();
173176
if (fixingTimes.length == 0) { //if there are no fixings return empty curve
174177
return new ForwardCurveInterpolation(FORWARD_EUR_6M,
@@ -213,8 +216,8 @@ private ForwardCurve get6MForwardCurve(final CalibrationContext ctx) {
213216
}
214217

215218
private ForwardCurve get1MForwardCurve(final CalibrationContext ctx) {
216-
double[] fixingTimes = fixings.stream().filter(x -> x.getCurveName().equals("Euribor1M")).map(x -> x.dateTime)
217-
.map(x -> FloatingpointDate.getFloatingPointDateFromDate(referenceDateTime, x))
219+
double[] fixingTimes = fixings.stream().filter(x -> x.getCurveName().equals("Euribor1M")).map(x -> x.dateTime.toLocalDate())
220+
.map(x -> FloatingpointDate.getFloatingPointDateFromDate(referenceDate, x))
218221
.mapToDouble(Double::doubleValue).sorted().toArray();
219222
if (fixingTimes.length == 0) { //if there are no fixings return empty curve
220223
return new ForwardCurveInterpolation("forward-EUR-1M",
@@ -257,4 +260,25 @@ private ForwardCurve get1MForwardCurve(final CalibrationContext ctx) {
257260
Arrays.stream(fixingTimes).min().orElseThrow() - 1.0 / 365.0,
258261
Arrays.stream(fixingTimes).max().orElseThrow() + 1.0 / 365.0);
259262
}
263+
264+
// CalibrationSpec for nearest €STR fixing as 1-day curve point
265+
private Optional<CalibratedCurves.CalibrationSpec> getCalibrationSpecOvernightRate(final CalibrationContext ctx) {
266+
List<CalibrationDataItem> estrFixings = fixings.stream().filter(fixingItem ->
267+
fixingItem.getSpec().getCurveName().equals("ESTR") &&
268+
fixingItem.getSpec().getMaturity().equals("1D")).toList();
269+
if (!estrFixings.isEmpty()) {
270+
CalibrationDataItem nearestFixing = estrFixings.stream()
271+
.min((item1, item2) -> {
272+
double diff1 = FloatingpointDate.getFloatingPointDateFromDate(ctx.getReferenceDateTime(), item1.getDateTime());
273+
double diff2 = FloatingpointDate.getFloatingPointDateFromDate(ctx.getReferenceDateTime(), item2.getDateTime());
274+
return Double.compare(Math.abs(diff1), Math.abs(diff2));
275+
})
276+
.orElse(null);
277+
if (nearestFixing != null) {
278+
return Optional.of(new CalibrationSpecProviderOvernightRate("1D", "tenor", nearestFixing.getQuote()).getCalibrationSpec(ctx));
279+
}
280+
}
281+
return Optional.empty();
282+
}
283+
260284
}

0 commit comments

Comments
 (0)