All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.finmath.smartcontract.product.xml.PlainSwapEditorHandler Maven / Gradle / Ivy

package net.finmath.smartcontract.product.xml;

import jakarta.xml.bind.*;
import net.finmath.marketdata.model.AnalyticModel;
import net.finmath.marketdata.model.AnalyticModelFromCurvesAndVols;
import net.finmath.marketdata.model.curves.Curve;
import net.finmath.marketdata.model.curves.ForwardCurveInterpolation;
import net.finmath.marketdata.model.curves.ForwardCurveWithFixings;
import net.finmath.modelling.descriptor.ScheduleDescriptor;
import net.finmath.smartcontract.model.*;
import net.finmath.smartcontract.product.SmartDerivativeContractDescriptor;
import net.finmath.smartcontract.valuation.marketdata.curvecalibration.*;
import net.finmath.time.FloatingpointDate;
import net.finmath.time.Period;
import net.finmath.time.Schedule;
import net.finmath.time.ScheduleGenerator;
import net.finmath.time.businessdaycalendar.BusinessdayCalendar;
import net.finmath.time.businessdaycalendar.BusinessdayCalendarExcludingTARGETHolidays;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Stream;

/**
 * Class that handles incoming requests for generating and interacting with plain swaps descriptors coming from the editor.
 * The API exposed by this class is not definitive and may be subject to changes without notice.
 *
 * @author Luca Bressan
 * @version alpha.1
 */
@SuppressWarnings("java:S125")
public final class PlainSwapEditorHandler { //TODO: this code needs some cleaning up

	private static final Logger logger = LoggerFactory.getLogger(PlainSwapEditorHandler.class);
	private static final String CONSTANT = "constant";
	private static final String FAILED_MODEL_CALIBRATION = "Failed to calibrate model.";
	private final Smartderivativecontract smartDerivativeContract;
	private final Schema sdcmlSchema;
	private final Marshaller marshaller;
	private final InterestRateStream floatingLeg;
	private final InterestRateStream fixedLeg;

	/**
	 * Returns the plain swap editor handler.
	 *
	 * @param plainSwapOperationRequest the JSON request that contains the editor info.
	 * @param templatePath              path for the SDCmL template XML to be used
	 * @param schemaPath                path for the SDCmL validation schema
	 * @throws IOException                    when loading settings files fails.
	 * @throws SAXException                   when validation of the generated contract fails.
	 * @throws JAXBException                  when marshalling/unmarshalling of a SDCmL object/file fails.
	 * @throws DatatypeConfigurationException when conversion of dates to the FPmL specifications fails.
	 */
	public PlainSwapEditorHandler(final PlainSwapOperationRequest plainSwapOperationRequest, String templatePath, String schemaPath) throws IOException, SAXException, JAXBException, DatatypeConfigurationException {
		try {
			final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
			sdcmlSchema = schemaFactory.newSchema((new ClassPathResource(schemaPath)).getURL());
		} catch (IOException | SAXException e) {
			logger.error("Failed to recover XSD schema. The file '{}' is missing, unreachable or invalid.", schemaPath);

			throw e;
		}
		final JAXBContext jaxbContext;
		try {
			jaxbContext = JAXBContext.newInstance("net.finmath.smartcontract.product.xml", this.getClass().getClassLoader());
		} catch (JAXBException e) {
			logger.error("Failed to load JAXB context.");

			throw e;
		}
		try {
			marshaller = jaxbContext.createMarshaller();
		} catch (JAXBException e) {
			logger.error("Failed to load JAXB marshaller.");

			throw e;
		}
		marshaller.setSchema(sdcmlSchema);
		final Unmarshaller unmarshaller;
		try {
			unmarshaller = jaxbContext.createUnmarshaller();
		} catch (JAXBException e) {
			logger.error("Failed to load JAXB un-marshaller.");

			throw e;
		}
		unmarshaller.setSchema(sdcmlSchema);
		try {
			marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
		} catch (PropertyException e) {
			logger.error("Failed to configure JAXB marshaller.");

			throw e;
		}

		// create new SDCmL file as object
		smartDerivativeContract = new Smartderivativecontract();
		smartDerivativeContract.setUniqueTradeIdentifier("test123");
		final Smartderivativecontract templateContract;
		try {
			ClassPathResource templateXmlResource = new ClassPathResource(templatePath);
			templateContract = (Smartderivativecontract) unmarshaller.unmarshal(templateXmlResource.getInputStream());
		} catch (JAXBException e) {
			logger.error("Failed to unmarshall the XML template file.");

			throw e;
		} catch (IOException e) {
			logger.error("An IO error occurred while unmarshalling the template file.");

			throw e;
		}

		// set the SDC specific stuff in the helper methods
		setSdcValuationHeader(smartDerivativeContract);
		setSdcPartiesHeader(plainSwapOperationRequest, smartDerivativeContract);
		setSdcSettlementHeader(plainSwapOperationRequest, smartDerivativeContract);


		// clone the template
		smartDerivativeContract.setUnderlyings(templateContract.getUnderlyings());

		final Trade trade = smartDerivativeContract.underlyings.underlying.dataDocument.trade.get(0);
		final XMLGregorianCalendar formattedTradeDate;
		try {
			formattedTradeDate = DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar.from(plainSwapOperationRequest.getTradeDate().toZonedDateTime()));
		} catch (DatatypeConfigurationException e) {
			logger.error("Failed to convert OffsetDateTime to XMLGregorianCalendar. This occurred while processing field tradeDate");

			throw e;
		}
		formattedTradeDate.setTimezone(DatatypeConstants.FIELD_UNDEFINED);
		trade.tradeHeader.tradeDate.setValue(formattedTradeDate);
		trade.tradeHeader.partyTradeIdentifier.get(0).tradeId = new TradeId();
		trade.tradeHeader.partyTradeIdentifier.get(0).tradeId.setValue("123456");
		trade.tradeHeader.partyTradeIdentifier.get(0).tradeId.setTradeIdScheme("test");
		trade.tradeHeader.partyTradeIdentifier.get(1).tradeId = new TradeId();
		trade.tradeHeader.partyTradeIdentifier.get(1).tradeId.setValue("123456");
		trade.tradeHeader.partyTradeIdentifier.get(1).tradeId.setTradeIdScheme("test");

		Swap swap = ((Swap) smartDerivativeContract.underlyings.underlying.dataDocument.trade.get(0).getProduct().getValue());

		Optional fixedLegOptional = swap.swapStream.stream().filter(PlainSwapEditorHandler::isFixedLeg).findFirst();
		if (fixedLegOptional.isEmpty())
			throw new IllegalStateException("The template has issues: failed to find valid candidate for fixed leg swapStream definition. I will fail now, sorry! :(");
		fixedLeg = fixedLegOptional.get();

		Optional floatingLegOptional = swap.swapStream.stream().filter(PlainSwapEditorHandler::isFloatingLeg).findFirst();
		if (floatingLegOptional.isEmpty())
			throw new IllegalStateException("The template has issues: failed to find valid candidate for floating leg swapStream definition. I will fail now, sorry! :(");
		floatingLeg = floatingLegOptional.get();

		// for each swap stream... (index is the order of appearance in the template)
		final XMLGregorianCalendar formattedEffectiveDate;
		try {
			formattedEffectiveDate = DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar.from(plainSwapOperationRequest.getEffectiveDate().toZonedDateTime()));
		} catch (DatatypeConfigurationException e) {
			logger.error("Failed to convert ZonedDateTime to XMLGregorianCalendar. This occurred while processing field effectiveDate");

			throw e;
		}
		formattedEffectiveDate.setTimezone(DatatypeConstants.FIELD_UNDEFINED);

		final XMLGregorianCalendar formattedTerminationDate;
		try {
			formattedTerminationDate = DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar.from(plainSwapOperationRequest.getTerminationDate().toZonedDateTime()));
		} catch (DatatypeConfigurationException e) {
			logger.error("Failed to convert ZonedDateTime to XMLGregorianCalendar. This occurred while processing field terminationDate");

			throw e;
		}
		formattedTerminationDate.setTimezone(DatatypeConstants.FIELD_UNDEFINED);
		for (InterestRateStream swapLeg : swap.swapStream) {

			swapLeg.calculationPeriodDates.effectiveDate.unadjustedDate.setValue(formattedEffectiveDate);
			swapLeg.calculationPeriodDates.terminationDate.unadjustedDate.setValue(formattedTerminationDate);


			swapLeg.calculationPeriodAmount.calculation.notionalSchedule.notionalStepSchedule.initialValue = BigDecimal.valueOf(plainSwapOperationRequest.getNotionalAmount());
			swapLeg.calculationPeriodAmount.calculation.notionalSchedule.notionalStepSchedule.currency.value = plainSwapOperationRequest.getCurrency();

		}

		if (plainSwapOperationRequest.getFloatingPayingParty().getFullName().equals(smartDerivativeContract.parties.party.get(0).name)) {
			floatingLeg.payerPartyReference.href = smartDerivativeContract.underlyings.underlying.dataDocument.party.get(0);
			floatingLeg.receiverPartyReference.href = smartDerivativeContract.underlyings.underlying.dataDocument.party.get(1);
			fixedLeg.payerPartyReference.href = smartDerivativeContract.underlyings.underlying.dataDocument.party.get(1);
			fixedLeg.receiverPartyReference.href = smartDerivativeContract.underlyings.underlying.dataDocument.party.get(0);
		} else {
			floatingLeg.payerPartyReference.href = smartDerivativeContract.underlyings.underlying.dataDocument.party.get(1);
			floatingLeg.receiverPartyReference.href = smartDerivativeContract.underlyings.underlying.dataDocument.party.get(0);
			fixedLeg.payerPartyReference.href = smartDerivativeContract.underlyings.underlying.dataDocument.party.get(0);
			fixedLeg.receiverPartyReference.href = smartDerivativeContract.underlyings.underlying.dataDocument.party.get(1);
		}


		floatingLeg.resetDates.fixingDates.periodMultiplier = BigInteger.valueOf(plainSwapOperationRequest.getFloatingFixingDayOffset().longValue());
		logger.info("Reading back floating fixing date offset: {}", floatingLeg.resetDates.fixingDates.periodMultiplier);
		floatingLeg.calculationPeriodAmount.calculation.dayCountFraction.value = plainSwapOperationRequest.getFloatingDayCountFraction();
		logger.info("Reading back floating day count fraction: {}", floatingLeg.calculationPeriodAmount.calculation.dayCountFraction.value);
		floatingLeg.paymentDates.paymentFrequency.periodMultiplier = BigInteger.valueOf(plainSwapOperationRequest.getFloatingPaymentFrequency().getPeriodMultiplier().longValue());
		logger.info("Reading back floating payment frequency period multiplier: {}", floatingLeg.paymentDates.paymentFrequency.periodMultiplier);
		floatingLeg.paymentDates.paymentFrequency.setPeriod(plainSwapOperationRequest.getFloatingPaymentFrequency().getPeriod());
		logger.info("Reading back floating payment frequency period: {}", floatingLeg.paymentDates.paymentFrequency.period);
		((FloatingRateCalculation) floatingLeg.calculationPeriodAmount.calculation.getRateCalculation().getValue()).floatingRateIndex.value = plainSwapOperationRequest.getFloatingRateIndex();
		logger.info("Reading back floating rate index: {}", ((FloatingRateCalculation) floatingLeg.calculationPeriodAmount.calculation.getRateCalculation().getValue()).floatingRateIndex.value);
		fixedLeg.calculationPeriodAmount.calculation.dayCountFraction.value = plainSwapOperationRequest.getFixedDayCountFraction();
		logger.info("Reading back fixed day count fraction {}", fixedLeg.calculationPeriodAmount.calculation.dayCountFraction.value);
		fixedLeg.calculationPeriodAmount.calculation.fixedRateSchedule.initialValue = BigDecimal.valueOf(plainSwapOperationRequest.getFixedRate()).setScale(12, RoundingMode.HALF_EVEN).divide(BigDecimal.valueOf(100L).setScale(12, RoundingMode.HALF_EVEN), RoundingMode.HALF_EVEN);
		logger.info("Reading back fixed rate: {}", fixedLeg.calculationPeriodAmount.calculation.fixedRateSchedule.initialValue);
		fixedLeg.paymentDates.paymentFrequency.periodMultiplier = BigInteger.valueOf(plainSwapOperationRequest.getFixedPaymentFrequency().getPeriodMultiplier().longValue());
		logger.info("Reading back fixed period multiplier: {}", fixedLeg.paymentDates.paymentFrequency.periodMultiplier);
		fixedLeg.paymentDates.paymentFrequency.period = plainSwapOperationRequest.getFixedPaymentFrequency().getPeriod();
		logger.info("Reading back fixed period: {}", fixedLeg.paymentDates.paymentFrequency.period);

		//TODO: ask people who know more about FPmL if the next lines are actually needed
		fixedLeg.calculationPeriodDates.calculationPeriodFrequency.periodMultiplier = BigInteger.valueOf(plainSwapOperationRequest.getFixedPaymentFrequency().getPeriodMultiplier().longValue());
		fixedLeg.calculationPeriodDates.calculationPeriodFrequency.setPeriod(plainSwapOperationRequest.getFixedPaymentFrequency().getPeriod());
		floatingLeg.calculationPeriodDates.calculationPeriodFrequency.periodMultiplier = BigInteger.valueOf(plainSwapOperationRequest.getFloatingPaymentFrequency().getPeriodMultiplier().longValue());
		floatingLeg.calculationPeriodDates.calculationPeriodFrequency.period = plainSwapOperationRequest.getFloatingPaymentFrequency().getPeriod();
		floatingLeg.calculationPeriodDates.calculationPeriodFrequency.setRollConvention("EOM");
		floatingLeg.resetDates.resetFrequency.periodMultiplier = BigInteger.valueOf(plainSwapOperationRequest.getFloatingPaymentFrequency().getPeriodMultiplier().longValue());
		floatingLeg.resetDates.resetFrequency.period = plainSwapOperationRequest.getFloatingPaymentFrequency().getPeriod();
		((FloatingRateCalculation) floatingLeg.calculationPeriodAmount.calculation.getRateCalculation().getValue()).indexTenor.periodMultiplier = BigInteger.valueOf(plainSwapOperationRequest.getFloatingPaymentFrequency().getPeriodMultiplier().longValue());
		((FloatingRateCalculation) floatingLeg.calculationPeriodAmount.calculation.getRateCalculation().getValue()).indexTenor.period = PeriodEnum.valueOf(plainSwapOperationRequest.getFloatingPaymentFrequency().getPeriod());
		// end of dubious lines


		smartDerivativeContract.receiverPartyID = "party2";

		logger.info("Instance built!");

	}

	private static boolean isFloatingLeg(InterestRateStream swapStream) {
		return swapStream.getCalculationPeriodAmount().getCalculation().getRateCalculation().getDeclaredType().equals(FloatingRateCalculation.class) && Objects.isNull(swapStream.getCalculationPeriodAmount().getCalculation().getFixedRateSchedule());
	}

	private static boolean isFixedLeg(InterestRateStream swapStream) {
		return !Objects.isNull(swapStream.getCalculationPeriodAmount().getCalculation().getFixedRateSchedule()) && Objects.isNull(swapStream.getCalculationPeriodAmount().getCalculation().getRateCalculation());
	}

	private static void setSdcSettlementHeader(final PlainSwapOperationRequest plainSwapOperationRequest, final Smartderivativecontract sdc) {
		Smartderivativecontract.Settlement settlementHeader = new Smartderivativecontract.Settlement();

		settlementHeader.setSettlementDateInitial(plainSwapOperationRequest.getTradeDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + "T12:00:00");
		settlementHeader.settlementTime = new Smartderivativecontract.Settlement.SettlementTime();
		settlementHeader.settlementTime.value = "17:00"; //taken from the template
		settlementHeader.settlementTime.type = "daily";
		settlementHeader.marketdata = new Smartderivativecontract.Settlement.Marketdata();
		settlementHeader.marketdata.provider = "refinitiv";
		Smartderivativecontract.Settlement.Marketdata.Marketdataitems marketDataItems = new Smartderivativecontract.Settlement.Marketdata.Marketdataitems();
		for (FrontendItemSpec marketDataItem : plainSwapOperationRequest.getValuationSymbols()) {
			Smartderivativecontract.Settlement.Marketdata.Marketdataitems.Item newItem = new Smartderivativecontract.Settlement.Marketdata.Marketdataitems.Item();
			newItem.curve = new ArrayList<>();
			newItem.symbol = new ArrayList<>();
			newItem.type = new ArrayList<>();
			newItem.tenor = new ArrayList<>();
			newItem.curve.add(marketDataItem.getCurve());
			newItem.symbol.add(marketDataItem.getSymbol());
			newItem.type.add(marketDataItem.getItemType());
			newItem.tenor.add(marketDataItem.getTenor());
			marketDataItems.getItem().add(newItem);
		}
		settlementHeader.marketdata.marketdataitems = marketDataItems;
		sdc.setSettlement(settlementHeader);
	}

	private static void setSdcPartiesHeader(final PlainSwapOperationRequest tradeDescriptor, final Smartderivativecontract smartDerivativeContract) {
		logger.info("Setting SDC header of response.");
		Smartderivativecontract.Parties parties = new Smartderivativecontract.Parties();
		List partyList = new ArrayList<>();

		Smartderivativecontract.Parties.Party party1 = new Smartderivativecontract.Parties.Party();
		logger.info("Setting id party1 for party {}", tradeDescriptor.getFirstCounterparty());
		party1.setName(tradeDescriptor.getFirstCounterparty().getFullName());
		party1.setId("party1");
		Smartderivativecontract.Parties.Party.MarginAccount marginAccount1 = new Smartderivativecontract.Parties.Party.MarginAccount();
		marginAccount1.setType(CONSTANT);
		marginAccount1.setValue(tradeDescriptor.getMarginBufferAmount().floatValue());
		Smartderivativecontract.Parties.Party.PenaltyFee penaltyFee1 = new Smartderivativecontract.Parties.Party.PenaltyFee();
		penaltyFee1.setType(CONSTANT);
		penaltyFee1.setValue(tradeDescriptor.getTerminationFeeAmount().floatValue());
		party1.setAddress("0x0");

		logger.info("Setting id party2 for party {}", tradeDescriptor.getSecondCounterparty());
		Smartderivativecontract.Parties.Party party2 = new Smartderivativecontract.Parties.Party();
		party2.setName(tradeDescriptor.getSecondCounterparty().getFullName());
		party2.setId("party2");
		Smartderivativecontract.Parties.Party.MarginAccount marginAccount2 = new Smartderivativecontract.Parties.Party.MarginAccount();
		marginAccount2.setType(CONSTANT);
		marginAccount2.setValue(tradeDescriptor.getMarginBufferAmount().floatValue());
		Smartderivativecontract.Parties.Party.PenaltyFee penaltyFee2 = new Smartderivativecontract.Parties.Party.PenaltyFee();
		penaltyFee2.setType(CONSTANT);
		penaltyFee2.setValue(tradeDescriptor.getTerminationFeeAmount().floatValue());
		party2.setAddress("0x0");

		party1.setMarginAccount(marginAccount1);
		party1.setPenaltyFee(penaltyFee1);
		party2.setMarginAccount(marginAccount2);
		party2.setPenaltyFee(penaltyFee2);

		partyList.add(party1);
		partyList.add(party2);
		parties.party = partyList;

		smartDerivativeContract.setParties(parties);
	}

	private static void setSdcValuationHeader(final Smartderivativecontract smartDerivativeContract) {
		Smartderivativecontract.Valuation valuationHeader = new Smartderivativecontract.Valuation();
		Smartderivativecontract.Valuation.Artefact artifactHeader = new Smartderivativecontract.Valuation.Artefact();
		artifactHeader.setGroupId("net.finmath");
		artifactHeader.setArtifactId("finmath-smart-derivative-contract");
		artifactHeader.setVersion("0.1.8");
		valuationHeader.setArtefact(artifactHeader);
		smartDerivativeContract.setValuation(valuationHeader);
	}

	/**
	 * Returns the SDCmL string associated with this plain swap handler.
	 *
	 * @return the SDCmL document
	 * @throws IOException   when the conversion of the stream to string fails.
	 * @throws SAXException  when the marshalled XML file does not validate against the schema.
	 * @throws JAXBException when the marshalling of the XML fails.
	 */
	public String getContractAsXmlString() throws IOException, SAXException, JAXBException {
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		try {
			marshaller.marshal(smartDerivativeContract, outputStream);
		} catch (JAXBException e) {
			logger.error("Failed to marshall out the generated XML. Check your inputs.");

			throw e;
		}
		// marshall xml out
		try {
			Validator validator = sdcmlSchema.newValidator();
			validator.validate(new StreamSource(new ByteArrayInputStream(outputStream.toByteArray())));
			logger.info("Validation successful!");
			// return outputStream.toString();

			// This solution is suboptimal.
			logger.info("XML was correctly generated, will now do some suboptimal text handling.");
			return outputStream.toString().replaceAll("", "").replaceAll("fpml:", "");

		} catch (SAXException e) {
			logger.error("Failed to validate the generated XML or some unrecoverable error occurred while validating.");
			logger.error("Details: {}", e.getMessage());
			throw e;
		} catch (IOException e) {
			logger.error("Failed to marshall out the generated XML file.");
			logger.error("Details: {}", e.getMessage());
			throw e;
		}

	}

	/**
	 * Getter method for the JAXB representation of the contract
	 *
	 * @return the contract object
	 */
	public Smartderivativecontract getContract() {
		return this.smartDerivativeContract;

	}

	/**
	 * Returns a list of cashflow periods representing the payment streams involved in the plain swap described.
	 *
	 * @param legSelector the leg for which the schedule should be calculated.
	 * @param marketData  the market data used for calibration of the model used for the payments' calculation.
	 * @return the payment schedule.
	 */
	public List getSchedule(LegSelector legSelector, String marketData) throws IOException, CloneNotSupportedException {
		InterestRateStream swapLeg;
		switch (legSelector) {
			case FIXED_LEG -> {
				swapLeg = fixedLeg;
				logger.info("Fixed leg detected.");
			}
			case FLOATING_LEG -> {
				swapLeg = floatingLeg;
				logger.info("Floating leg detected.");
			}
			default -> throw new IllegalArgumentException("Failed to detect leg type");
		}
		final LocalDate startDate = swapLeg.getCalculationPeriodDates().getEffectiveDate().getUnadjustedDate().getValue().toGregorianCalendar().toZonedDateTime().toLocalDate();
		logger.info("Start date detected: {}", startDate);
		final LocalDate maturityDate = swapLeg.getCalculationPeriodDates().getTerminationDate().getUnadjustedDate().getValue().toGregorianCalendar().toZonedDateTime().toLocalDate();
		logger.info("Maturity date detected: {}", maturityDate);
		int fixingOffsetDays = 0;
		try {
			fixingOffsetDays = swapLeg.getResetDates().getFixingDates().getPeriodMultiplier().intValue();
		} catch (NullPointerException npe) {
			logger.warn("No fixing offset was detected, 0 implied.");

		}
		int paymentOffsetDays = 0;
		try {
			paymentOffsetDays = swapLeg.getPaymentDates().getPaymentDaysOffset().getPeriodMultiplier().intValue();
		} catch (NullPointerException npe) {
			logger.warn("No payment offset was detected, 0 implied.");

		}


		final BusinessdayCalendar.DateRollConvention dateRollConvention;
		switch (swapLeg.getPaymentDates().getPaymentDatesAdjustments().getBusinessDayConvention()) {
			case PRECEDING -> dateRollConvention = BusinessdayCalendar.DateRollConvention.PRECEDING;
			case MODPRECEDING -> dateRollConvention = BusinessdayCalendar.DateRollConvention.MODIFIED_PRECEDING;
			case FOLLOWING -> dateRollConvention = BusinessdayCalendar.DateRollConvention.FOLLOWING;
			case MODFOLLOWING -> dateRollConvention = BusinessdayCalendar.DateRollConvention.MODIFIED_FOLLOWING;
			case NONE -> dateRollConvention = BusinessdayCalendar.DateRollConvention.UNADJUSTED;
			default ->
				throw new IllegalArgumentException("Unrecognized date roll convention: " + swapLeg.getPaymentDates().getPaymentDatesAdjustments().getBusinessDayConvention());
		}

		logger.info("Date roll convention detected: {}", dateRollConvention);

		final ScheduleGenerator.DaycountConvention daycountConvention = ScheduleGenerator.DaycountConvention.getEnum(swapLeg.getCalculationPeriodAmount().getCalculation().getDayCountFraction().getValue());
		ScheduleGenerator.Frequency frequency = null;
		final int multiplier = swapLeg.getPaymentDates().getPaymentFrequency().getPeriodMultiplier().intValue();

		logger.info("Reading period symbol: {}", swapLeg.getPaymentDates().getPaymentFrequency().getPeriod());
		switch (swapLeg.getPaymentDates().getPaymentFrequency().getPeriod()) {
			case "D" -> {
				if (multiplier == 1) {
					frequency = ScheduleGenerator.Frequency.DAILY;
				}
			}
			case "Y" -> {
				if (multiplier == 1) {
					frequency = ScheduleGenerator.Frequency.ANNUAL;
				}
			}
			case "M" -> frequency = switch (multiplier) {
				case 1 -> ScheduleGenerator.Frequency.MONTHLY;
				case 3 -> ScheduleGenerator.Frequency.QUARTERLY;
				case 6 -> ScheduleGenerator.Frequency.SEMIANNUAL;
				default ->
					throw new IllegalArgumentException("Unknown periodMultiplier " + swapLeg.getPaymentDates().getPaymentFrequency().getPeriodMultiplier().intValue() + ".");
			};
			default ->
				throw new IllegalArgumentException("Unknown period " + swapLeg.getPaymentDates().getPaymentFrequency().getPeriod() + ".");
		}

		//build schedule
		logger.info("Payment frequency detected: {}", Objects.requireNonNull(frequency));
		final ScheduleDescriptor scheduleDescriptor = new ScheduleDescriptor(startDate, maturityDate, frequency, daycountConvention, ScheduleGenerator.ShortPeriodConvention.LAST, dateRollConvention, new BusinessdayCalendarExcludingTARGETHolidays(), fixingOffsetDays, paymentOffsetDays);


		List cashflowPeriods = new ArrayList<>();
		Schedule schedule = scheduleDescriptor.getSchedule(this.smartDerivativeContract.underlyings.underlying.dataDocument.trade.get(0).tradeHeader.tradeDate.value.toGregorianCalendar().toZonedDateTime().toLocalDate());
		double notional = swapLeg.calculationPeriodAmount.calculation.notionalSchedule.notionalStepSchedule.initialValue.doubleValue();

		/* Check the product */
		String forwardCurveID = "forward-EUR-6M";
		String discountCurveID = "discount-EUR-OIS";
		AnalyticModel calibratedModel = getAnalyticModel(marketData, schedule, forwardCurveID, discountCurveID);

		String currency = swapLeg.calculationPeriodAmount.calculation.notionalSchedule.notionalStepSchedule.currency.value;
		int i = 0;
		for (Period schedulePeriod : schedule) {
			double rate = legSelector.equals(LegSelector.FIXED_LEG) ? swapLeg.calculationPeriodAmount.calculation.fixedRateSchedule.initialValue.doubleValue() : calibratedModel.getForwardCurve(forwardCurveID).getForward(calibratedModel, schedule.getFixing(i));
			double homePartyIsPayerPartyFactor = ((Party) swapLeg.payerPartyReference.href).id.equals(this.smartDerivativeContract.receiverPartyID) ? 1.0 : -1.0;

			cashflowPeriods.add(new CashflowPeriod().cashflow(new ValueResult().currency(currency).valuationDate(new Date().toString()).value(BigDecimal.valueOf(homePartyIsPayerPartyFactor * schedule.getPeriodLength(i) * notional * rate))).fixingDate(OffsetDateTime.of(schedulePeriod.getFixing(), LocalTime.NOON, ZoneOffset.UTC)).paymentDate(OffsetDateTime.of(schedulePeriod.getPayment(), LocalTime.NOON, ZoneOffset.UTC)).periodStart(OffsetDateTime.of(schedulePeriod.getPeriodStart(), LocalTime.NOON, ZoneOffset.UTC)).periodEnd(OffsetDateTime.of(schedulePeriod.getPeriodEnd(), LocalTime.NOON, ZoneOffset.UTC)).rate(rate));

			i++;
		}

		return cashflowPeriods;


	}

	/**
	 * Returns a list of cashflow periods representing the payment streams involved in the plain swap described.
	 *
	 * @param legSelector the leg for which the schedule should be calculated.
	 * @param marketData  the market data used for calibration of the model used for the payments' calculation.
	 * @return the payment schedule.
	 */
	public List getSchedule(LegSelector legSelector, MarketDataSet marketData) throws IOException, CloneNotSupportedException {
		InterestRateStream swapLeg;
		switch (legSelector) {
			case FIXED_LEG -> {
				swapLeg = fixedLeg;
				logger.info("Fixed leg detected.");
			}
			case FLOATING_LEG -> {
				swapLeg = floatingLeg;
				logger.info("Floating leg detected.");
			}
			default -> throw new IllegalArgumentException("Failed to detect leg type");
		}
		final LocalDate startDate = swapLeg.getCalculationPeriodDates().getEffectiveDate().getUnadjustedDate().getValue().toGregorianCalendar().toZonedDateTime().toLocalDate();
		logger.info("Start date detected: {}", startDate);
		final LocalDate maturityDate = swapLeg.getCalculationPeriodDates().getTerminationDate().getUnadjustedDate().getValue().toGregorianCalendar().toZonedDateTime().toLocalDate();
		logger.info("Maturity date detected: {}", maturityDate);
		int fixingOffsetDays = 0;
		try {
			fixingOffsetDays = swapLeg.getResetDates().getFixingDates().getPeriodMultiplier().intValue();
		} catch (NullPointerException npe) {
			logger.warn("No fixing offset was detected, 0 implied.");

		}
		int paymentOffsetDays = 0;
		try {
			paymentOffsetDays = swapLeg.getPaymentDates().getPaymentDaysOffset().getPeriodMultiplier().intValue();
		} catch (NullPointerException npe) {
			logger.warn("No payment offset was detected, 0 implied.");

		}


		final BusinessdayCalendar.DateRollConvention dateRollConvention;
		switch (swapLeg.getPaymentDates().getPaymentDatesAdjustments().getBusinessDayConvention()) {
			case PRECEDING -> dateRollConvention = BusinessdayCalendar.DateRollConvention.PRECEDING;
			case MODPRECEDING -> dateRollConvention = BusinessdayCalendar.DateRollConvention.MODIFIED_PRECEDING;
			case FOLLOWING -> dateRollConvention = BusinessdayCalendar.DateRollConvention.FOLLOWING;
			case MODFOLLOWING -> dateRollConvention = BusinessdayCalendar.DateRollConvention.MODIFIED_FOLLOWING;
			case NONE -> dateRollConvention = BusinessdayCalendar.DateRollConvention.UNADJUSTED;
			default ->
				throw new IllegalArgumentException("Unrecognized date roll convention: " + swapLeg.getPaymentDates().getPaymentDatesAdjustments().getBusinessDayConvention());
		}

		logger.info("Date roll convention detected: {}", dateRollConvention);

		final ScheduleGenerator.DaycountConvention daycountConvention = ScheduleGenerator.DaycountConvention.getEnum(swapLeg.getCalculationPeriodAmount().getCalculation().getDayCountFraction().getValue());
		ScheduleGenerator.Frequency frequency = null;
		final int multiplier = swapLeg.getPaymentDates().getPaymentFrequency().getPeriodMultiplier().intValue();

		logger.info("Reading period symbol: {}", swapLeg.getPaymentDates().getPaymentFrequency().getPeriod());
		switch (swapLeg.getPaymentDates().getPaymentFrequency().getPeriod()) {
			case "D" -> {
				if (multiplier == 1) {
					frequency = ScheduleGenerator.Frequency.DAILY;
				}
			}
			case "Y" -> {
				if (multiplier == 1) {
					frequency = ScheduleGenerator.Frequency.ANNUAL;
				}
			}
			case "M" -> frequency = switch (multiplier) {
				case 1 -> ScheduleGenerator.Frequency.MONTHLY;
				case 3 -> ScheduleGenerator.Frequency.QUARTERLY;
				case 6 -> ScheduleGenerator.Frequency.SEMIANNUAL;
				default ->
					throw new IllegalArgumentException("Unknown periodMultiplier " + swapLeg.getPaymentDates().getPaymentFrequency().getPeriodMultiplier().intValue() + ".");
			};
			default ->
				throw new IllegalArgumentException("Unknown period " + swapLeg.getPaymentDates().getPaymentFrequency().getPeriod() + ".");
		}

		//build schedule
		logger.info("Payment frequency detected: {}", Objects.requireNonNull(frequency));
		final ScheduleDescriptor scheduleDescriptor = new ScheduleDescriptor(startDate, maturityDate, frequency, daycountConvention, ScheduleGenerator.ShortPeriodConvention.LAST, dateRollConvention, new BusinessdayCalendarExcludingTARGETHolidays(), fixingOffsetDays, paymentOffsetDays);


		List cashflowPeriods = new ArrayList<>();
		Schedule schedule = scheduleDescriptor.getSchedule(this.smartDerivativeContract.underlyings.underlying.dataDocument.trade.get(0).tradeHeader.tradeDate.value.toGregorianCalendar().toZonedDateTime().toLocalDate());

		double notional = swapLeg.calculationPeriodAmount.calculation.notionalSchedule.notionalStepSchedule.initialValue.doubleValue();

		/* Check the product */
		String forwardCurveID = "forward-EUR-6M";
		String discountCurveID = "discount-EUR-OIS";
		AnalyticModel calibratedModel = getAnalyticModel(marketData, schedule, forwardCurveID, discountCurveID);

		String currency = swapLeg.calculationPeriodAmount.calculation.notionalSchedule.notionalStepSchedule.currency.value;
		double timeDiff = FloatingpointDate.getFloatingPointDateFromDate(
				marketData.getRequestTimestamp().toLocalDate().atStartOfDay(),
				marketData.getRequestTimestamp().toLocalDateTime());
		int i = 0;
		for (Period schedulePeriod : schedule) {
			double rate = legSelector.equals(LegSelector.FIXED_LEG) ? swapLeg.calculationPeriodAmount.calculation.fixedRateSchedule.initialValue.doubleValue() : calibratedModel.getForwardCurve(forwardCurveID).getForward(calibratedModel, schedule.getFixing(i) + timeDiff);

			double homePartyIsPayerPartyFactor = ((Party) swapLeg.payerPartyReference.href).id.equals(this.smartDerivativeContract.receiverPartyID) ? 1.0 : -1.0;

			cashflowPeriods.add(new CashflowPeriod().cashflow(new ValueResult().currency(currency).valuationDate(new Date().toString()).value(BigDecimal.valueOf(homePartyIsPayerPartyFactor * schedule.getPeriodLength(i) * notional * rate))).fixingDate(OffsetDateTime.of(schedulePeriod.getFixing(), LocalTime.NOON, ZoneOffset.UTC)).paymentDate(OffsetDateTime.of(schedulePeriod.getPayment(), LocalTime.NOON, ZoneOffset.UTC)).periodStart(OffsetDateTime.of(schedulePeriod.getPeriodStart(), LocalTime.NOON, ZoneOffset.UTC)).periodEnd(OffsetDateTime.of(schedulePeriod.getPeriodEnd(), LocalTime.NOON, ZoneOffset.UTC)).rate(rate));

			i++;
		}

		return cashflowPeriods;


	}

	private AnalyticModel getAnalyticModel(String marketData, Schedule schedule, String forwardCurveID, String discountCurveID) throws IOException, CloneNotSupportedException {         // TODO: ask Christian or Peter to review this
		List marketDataSets;
		try {
			marketDataSets = CalibrationParserDataItems.getScenariosFromJsonString(marketData);
		} catch (IllegalArgumentException e) {
			logger.error("Failed to load market data.", e);
			throw e;
		}
		Validate.isTrue(marketDataSets.size() == 1, "Parameter marketData should be only a single market data set");

		LocalDateTime marketDataTime = marketDataSets.get(0).getDate();

		final Optional optionalScenario = marketDataSets.stream().filter(scenario -> scenario.getDate().equals(marketDataTime)).findAny();
		final CalibrationDataset scenario;
		if (optionalScenario.isPresent()) scenario = optionalScenario.get();
		else throw new IllegalStateException("Failed to load calibration dataset.");

		final LocalDate referenceDate = marketDataTime.toLocalDate();

		final CalibrationParserDataItems parser = new CalibrationParserDataItems();
		final Calibrator calibrator = /*new Calibrator();*/ null;

		final Stream allCalibrationItems = scenario.getDataAsCalibrationDataPointStream(parser);


		final Optional optionalCalibrationResult;
		try {
			optionalCalibrationResult = calibrator.calibrateModel(allCalibrationItems, new CalibrationContextImpl(referenceDate, 1E-9));
		} catch (CloneNotSupportedException e) {
			logger.error(FAILED_MODEL_CALIBRATION);
			throw e;
		}
		AnalyticModel calibratedModel;
		if (optionalCalibrationResult.isPresent())
			calibratedModel = optionalCalibrationResult.get().getCalibratedModel();
		else throw new IllegalStateException(FAILED_MODEL_CALIBRATION);


		Set pastFixings = scenario.getFixingDataItems();

		// @Todo what if we have no past fixing provided
		// @Todo what when we are exactly on the fixing date but before 11:00 am.
		ForwardCurveInterpolation fixedCurve = this.getCurvePastFixings("fixedCurve", referenceDate, calibratedModel, discountCurveID, pastFixings);//ForwardCurveInterpolation.createForwardCurveFromForwards("pastFixingCurve", pastFixingTimeArray, pastFixingArray, paymentOffset);
		Curve forwardCurveWithFixings = new ForwardCurveWithFixings(calibratedModel.getForwardCurve(forwardCurveID), fixedCurve, schedule.getFixing(0), 0.0);
		Curve[] finalCurves = {calibratedModel.getDiscountCurve(discountCurveID), calibratedModel.getForwardCurve(forwardCurveID), forwardCurveWithFixings};
		calibratedModel = new AnalyticModelFromCurvesAndVols(referenceDate, finalCurves);
		return calibratedModel;
	}

	private AnalyticModel getAnalyticModel(MarketDataSet marketData, Schedule schedule, String forwardCurveID, String discountCurveID) throws IOException, CloneNotSupportedException {         // TODO: ask Christian or Peter to review this
		SmartDerivativeContractDescriptor productDescriptor = null;
		try {
			productDescriptor = SDCXMLParser.parse(this.getContractAsXmlString());
		} catch (ParserConfigurationException | SAXException | JAXBException e) {
			throw new SDCException(ExceptionId.SDC_XML_PARSE_ERROR, e.getMessage());
		}

		Set cdi = new HashSet<>();


		List mdReferences = productDescriptor.getMarketdataItemList();
		List mdValues = marketData.getValues();
		for (CalibrationDataItem.Spec mdr : mdReferences) {
			for (MarketDataSetValuesInner mdv : mdValues) {
				if (mdv.getSymbol().equals(mdr.getKey())) {
					cdi.add(
							new CalibrationDataItem(mdr, mdv.getValue(), mdv.getDataTimestamp().toLocalDateTime())
					);
				}
			}
		}

		List marketDataSets = new ArrayList<>();
		marketDataSets.add(new CalibrationDataset(cdi, marketData.getRequestTimestamp().toLocalDateTime()));

		LocalDateTime marketDataTime = marketData.getRequestTimestamp().toLocalDateTime();

		final Optional optionalScenario = marketDataSets.stream().filter(scenario -> scenario.getDate().equals(marketDataTime)).findAny();
		final CalibrationDataset scenario;
		if (optionalScenario.isPresent()) scenario = optionalScenario.get();
		else throw new IllegalStateException("Failed to load calibration dataset.");

		final LocalDate referenceDate = marketDataTime.toLocalDate();

		final CalibrationParserDataItems parser = new CalibrationParserDataItems();


		final Stream allCalibrationItems = scenario.getDataAsCalibrationDataPointStream(parser);
		Calibrator calibrator = new Calibrator(scenario.getDataPoints().stream().filter(
				ci -> ci.getSpec().getProductName().equals("Fixing") || ci.getSpec().getProductName().equals(
						"Deposit")).toList(), new CalibrationContextImpl(referenceDate, 1E-9));

		final Optional optionalCalibrationResult;

		try {
			optionalCalibrationResult = calibrator.calibrateModel(allCalibrationItems, new CalibrationContextImpl(referenceDate, 1E-9));
		} catch (CloneNotSupportedException e) {
			logger.error(FAILED_MODEL_CALIBRATION);
			throw e;
		}
		AnalyticModel calibratedModel;
		if (optionalCalibrationResult.isPresent())
			calibratedModel = optionalCalibrationResult.get().getCalibratedModel();
		else throw new IllegalStateException(FAILED_MODEL_CALIBRATION);
		return calibratedModel;
	}

	private ForwardCurveInterpolation getCurvePastFixings(final String curveID, LocalDate referenceDate, AnalyticModel model, String discountCurveName, final Set pastFixings) {
		Map fixingMap = new LinkedHashMap<>();
		pastFixings.forEach(item -> fixingMap.put(FloatingpointDate.getFloatingPointDateFromDate(referenceDate, item.getDate()), item.getQuote()));
		double[] pastFixingTimes = fixingMap.keySet().stream().mapToDouble(time -> time).toArray();
		double[] pastFixingsValues = Arrays.stream(pastFixingTimes).map(fixingMap::get).toArray();
		ForwardCurveInterpolation.InterpolationEntityForward interpolationEntityForward = ForwardCurveInterpolation.InterpolationEntityForward.FORWARD;
		return ForwardCurveInterpolation.createForwardCurveFromForwards(curveID, referenceDate, "offsetcode", interpolationEntityForward, discountCurveName, model, pastFixingTimes, pastFixingsValues);
	}

	/**
	 * Enumeration of possible choices for the swap legs.
	 */
	public enum LegSelector {
		FIXED_LEG, FLOATING_LEG
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy