/** * Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.loader.fpml; import static com.opengamma.strata.collect.Guavate.toImmutableList; import java.time.LocalDate; import java.time.LocalTime; import java.time.Period; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; import com.opengamma.strata.basics.ReferenceData; import com.opengamma.strata.basics.StandardId; import com.opengamma.strata.basics.currency.Currency; import com.opengamma.strata.basics.currency.CurrencyAmount; import com.opengamma.strata.basics.date.AdjustableDate; import com.opengamma.strata.basics.date.BusinessDayAdjustment; import com.opengamma.strata.basics.date.BusinessDayConvention; import com.opengamma.strata.basics.date.DayCount; import com.opengamma.strata.basics.date.DaysAdjustment; import com.opengamma.strata.basics.date.HolidayCalendarId; import com.opengamma.strata.basics.date.HolidayCalendarIds; import com.opengamma.strata.basics.date.Tenor; import com.opengamma.strata.basics.index.FloatingRateName; import com.opengamma.strata.basics.index.Index; import com.opengamma.strata.basics.index.PriceIndex; import com.opengamma.strata.basics.schedule.Frequency; import com.opengamma.strata.basics.schedule.RollConvention; import com.opengamma.strata.collect.Messages; import com.opengamma.strata.collect.io.XmlElement; import com.opengamma.strata.product.TradeInfo; import com.opengamma.strata.product.TradeInfoBuilder; import com.opengamma.strata.product.common.BuySell; import com.opengamma.strata.product.common.PayReceive; /** * Provides data about the whole FpML document and parse helper methods. * <p> * This is primarily used to support implementations of {@link FpmlParserPlugin}. * See {@link FpmlDocumentParser} for the main entry point for FpML parsing. */ public final class FpmlDocument { // FRN definition is dates that are on same numerical day of month // Use last business day of month if no matching numerical date (eg. 31st June replaced by last business day of June) // Non-business days are replaced by following, or preceding to avoid changing the month // If last date was last business day of month, then all subsequent dates are last business day of month // While close to ModifiedFollowing, it is unclear is that is correct for BusinessDayConvention // FpML also has a 'NotApplicable' option, which probably should map to null in the caller private static final Logger log = LoggerFactory.getLogger(FpmlDocument.class); /** * The 'id' attribute key. */ public static final String ID = "id"; /** * The 'href' attribute key. */ public static final String HREF = "href"; /** * Scheme used for parties that are read from FpML. */ private static final String FPML_PARTY_SCHEME = "FpML-partyId"; /** * The enum group for FpML conversions. */ private static final String ENUM_FPML = "FpML"; /** * The FpML date parser. */ private static final DateTimeFormatter FPML_DATE_FORMAT = DateTimeFormatter.ofPattern("uuuu-MM-dd[XXX]", Locale.ENGLISH); /** * The map of frequencies, designed to normalize and reduce object creation. */ private static final Map<String, Frequency> FREQUENCY_MAP = ImmutableMap.<String, Frequency>builder() .put("1D", Frequency.P12M) .put("7D", Frequency.P1W) .put("14D", Frequency.P2W) .put("28D", Frequency.P4W) .put("91D", Frequency.P13W) .put("182D", Frequency.P26W) .put("364D", Frequency.P52W) .put("1W", Frequency.P1W) .put("2W", Frequency.P2W) .put("4W", Frequency.P4W) .put("13W", Frequency.P13W) .put("26W", Frequency.P26W) .put("52W", Frequency.P52W) .put("1M", Frequency.P1M) .put("2M", Frequency.P2M) .put("3M", Frequency.P3M) .put("4M", Frequency.P4M) .put("6M", Frequency.P6M) .put("12M", Frequency.P12M) .put("1Y", Frequency.P12M) .build(); /** * The map of index tenors, designed to normalize and reduce object creation. */ private static final Map<String, Tenor> TENOR_MAP = ImmutableMap.<String, Tenor>builder() .put("7D", Tenor.TENOR_1W) .put("14D", Tenor.TENOR_2W) .put("21D", Tenor.TENOR_3W) .put("28D", Tenor.TENOR_4W) .put("1W", Tenor.TENOR_1W) .put("2W", Tenor.TENOR_2W) .put("1M", Tenor.TENOR_1M) .put("2M", Tenor.TENOR_2M) .put("3M", Tenor.TENOR_3M) .put("6M", Tenor.TENOR_6M) .put("12M", Tenor.TENOR_12M) .put("1Y", Tenor.TENOR_12M) .build(); /** * The map of holiday calendar ids to zone ids. */ private static final Map<String, ZoneId> HOLIDAY_CALENDARID_MAP = ImmutableMap.<String, ZoneId>builder() .put("BEBR", ZoneId.of("Europe/Brussels")) .put("CATO", ZoneId.of("America/Toronto")) .put("CHZU", ZoneId.of("Europe/Zurich")) .put("DEFR", ZoneId.of("Europe/Berlin")) .put("FRPA", ZoneId.of("Europe/Paris")) .put("GBLO", ZoneId.of("Europe/London")) .put("JPTO", ZoneId.of("Asia/Tokyo")) .put("USNY", ZoneId.of("America/New_York")) .build(); /** * Constant defining the "any" selector. * This must be defined as a constant so that == works when comparing it. * FpmlPartySelector is an interface and can only define public constants, thus it is declared here. */ static final FpmlPartySelector ANY_SELECTOR = allParties -> Optional.empty(); /** * Constant defining the "standard" trade info parser. */ static final FpmlTradeInfoParserPlugin TRADE_INFO_STANDARD = (doc, tradeDate, allTradeIds) -> { TradeInfoBuilder builder = TradeInfo.builder(); builder.tradeDate(tradeDate); builder.id(allTradeIds.get(doc.getOurPartyHrefId()).stream().findFirst().orElse(null)); return builder; }; /** * The parsed file. */ private final XmlElement fpmlRoot; /** * The map of references. */ private final ImmutableMap<String, XmlElement> references; /** * Map of reference id to partyId. */ private final ImmutableListMultimap<String, String> parties; /** * The party reference id. */ private final String ourPartyHrefId; /** * The trade info builder. */ private final FpmlTradeInfoParserPlugin tradeInfoParser; /** * The reference data. */ private final ReferenceData refData; //------------------------------------------------------------------------- /** * Creates an instance, based on the specified element. * <p> * The map of references is used to link one part of the XML to another. * For example, if one part of the XML has {@code <foo id="fooId">}, the references * map will contain an entry mapping "fooId" to the parsed element {@code <foo>}. * * @param fpmlRootEl the source of the FpML XML document * @param references the map of id/href to referenced element * @param ourPartySelector the selector used to find "our" party within the set of parties in the FpML document * @param tradeInfoParser the trade info parser * @param refData the reference data to use */ public FpmlDocument( XmlElement fpmlRootEl, Map<String, XmlElement> references, FpmlPartySelector ourPartySelector, FpmlTradeInfoParserPlugin tradeInfoParser, ReferenceData refData) { this.fpmlRoot = fpmlRootEl; this.references = ImmutableMap.copyOf(references); this.parties = parseParties(fpmlRootEl); this.ourPartyHrefId = findOurParty(ourPartySelector); this.tradeInfoParser = tradeInfoParser; this.refData = refData; } // parse all the root-level party elements private static ImmutableListMultimap<String, String> parseParties(XmlElement root) { ListMultimap<String, String> parties = ArrayListMultimap.create(); for (XmlElement child : root.getChildren("party")) { parties.putAll(child.getAttribute(ID), findPartyIds(child)); } return ImmutableListMultimap.copyOf(parties); } // find the party identifiers private static List<String> findPartyIds(XmlElement party) { ImmutableList.Builder<String> builder = ImmutableList.builder(); for (XmlElement child : party.getChildren("partyId")) { if (child.hasContent()) { builder.add(child.getContent()); } } return builder.build(); } // locate our party href/id reference private String findOurParty(FpmlPartySelector ourPartySelector) { // check for "any" selector to avoid logging message in normal case if (ourPartySelector == FpmlPartySelector.any()) { return ""; } Optional<String> selected = ourPartySelector.selectParty(parties); if (selected.isPresent()) { String selectedId = selected.get(); if (!parties.keySet().contains(selectedId)) { throw new FpmlParseException(Messages.format( "Selector returned an ID '{}' that is not present in the document: {}", selectedId, parties)); } return selectedId; } log.warn("Failed to resolve \"our\" counterparty from FpML document, using leg defaults instead: " + parties); return ""; } //------------------------------------------------------------------------- /** * Gets the FpML root element. * <p> * This is not necessarily the root of the whole document. * * @return the FpML root element */ public XmlElement getFpmlRoot() { return fpmlRoot; } /** * Gets the map of href/id references. * * @return the reference map */ public ImmutableMap<String, XmlElement> getReferences() { return references; } /** * Gets the map of party identifiers keyed by href/id reference. * * @return the party map */ public ImmutableListMultimap<String, String> getParties() { return parties; } /** * Gets our party href/id reference. * <p> * This is used to identify the direction of the trade. * * @return our party, empty if not known */ public String getOurPartyHrefId() { return ourPartyHrefId; } /** * Gets the reference data. * <p> * Use of reference data is not necessary to parse most FpML documents. * It is only needed to handle some edge cases, notably around relative dates. * * @return the reference data */ public ReferenceData getReferenceData() { return refData; } //------------------------------------------------------------------------- /** * Parses the trade header element. * <p> * This parses the trade date and identifier. * * @param tradeEl the trade element * @return the trade info builder * @throws RuntimeException if unable to parse */ public TradeInfoBuilder parseTradeInfo(XmlElement tradeEl) { XmlElement tradeHeaderEl = tradeEl.getChild("tradeHeader"); LocalDate tradeDate = parseDate(tradeHeaderEl.getChild("tradeDate")); return tradeInfoParser.parseTrade(this, tradeDate, parseAllTradeIds(tradeHeaderEl)); } // find all trade IDs by party herf id private ListMultimap<String, StandardId> parseAllTradeIds(XmlElement tradeHeaderEl) { // look through each partyTradeIdentifier ListMultimap<String, StandardId> allIds = ArrayListMultimap.create(); List<XmlElement> partyTradeIdentifierEls = tradeHeaderEl.getChildren("partyTradeIdentifier"); for (XmlElement partyTradeIdentifierEl : partyTradeIdentifierEls) { Optional<XmlElement> partyRefOptEl = partyTradeIdentifierEl.findChild("partyReference"); if (partyRefOptEl.isPresent() && partyRefOptEl.get().findAttribute(HREF).isPresent()) { String partyHref = partyRefOptEl.get().findAttribute(HREF).get(); // try to find a tradeId, either in versionedTradeId or as a direct child Optional<XmlElement> vtradeIdOptEl = partyTradeIdentifierEl.findChild("versionedTradeId"); Optional<XmlElement> tradeIdOptEl = vtradeIdOptEl .map(vt -> Optional.of(vt.getChild("tradeId"))) .orElse(partyTradeIdentifierEl.findChild("tradeId")); if (tradeIdOptEl.isPresent() && tradeIdOptEl.get().findAttribute("tradeIdScheme").isPresent()) { XmlElement tradeIdEl = tradeIdOptEl.get(); String scheme = tradeIdEl.getAttribute("tradeIdScheme"); // ignore if there is an empty scheme or value if (!scheme.isEmpty() && !tradeIdEl.getContent().isEmpty()) { allIds.put(partyHref, StandardId.of(StandardId.encodeScheme(scheme), tradeIdEl.getContent())); } } } } return allIds; } //------------------------------------------------------------------------- /** * Converts an FpML 'BuyerSeller.model' to a {@code BuySell}. * <p> * The {@link TradeInfo} builder is updated with the counterparty. * * @param baseEl the FpML payer receiver model element * @param tradeInfoBuilder the builder of the trade info * @return the pay/receive flag * @throws RuntimeException if unable to parse */ public BuySell parseBuyerSeller(XmlElement baseEl, TradeInfoBuilder tradeInfoBuilder) { String buyerPartyReference = baseEl.getChild("buyerPartyReference").getAttribute(FpmlDocument.HREF); String sellerPartyReference = baseEl.getChild("sellerPartyReference").getAttribute(FpmlDocument.HREF); if (ourPartyHrefId.isEmpty() || buyerPartyReference.equals(ourPartyHrefId)) { tradeInfoBuilder.counterparty(StandardId.of(FPML_PARTY_SCHEME, parties.get(sellerPartyReference).get(0))); return BuySell.BUY; } else if (sellerPartyReference.equals(ourPartyHrefId)) { tradeInfoBuilder.counterparty(StandardId.of(FPML_PARTY_SCHEME, parties.get(buyerPartyReference).get(0))); return BuySell.SELL; } else { throw new FpmlParseException(Messages.format( "Neither buyerPartyReference nor sellerPartyReference contain our party ID: {}", ourPartyHrefId)); } } /** * Converts an FpML 'PayerReceiver.model' to a {@code PayReceive}. * <p> * The {@link TradeInfo} builder is updated with the counterparty. * * @param baseEl the FpML payer receiver model element * @param tradeInfoBuilder the builder of the trade info * @return the pay/receive flag * @throws RuntimeException if unable to parse */ public PayReceive parsePayerReceiver(XmlElement baseEl, TradeInfoBuilder tradeInfoBuilder) { String payerPartyReference = baseEl.getChild("payerPartyReference").getAttribute(HREF); String receiverPartyReference = baseEl.getChild("receiverPartyReference").getAttribute(HREF); Object currentCounterparty = tradeInfoBuilder.build().getCounterparty().orElse(null); // determine direction and setup counterparty if ((ourPartyHrefId.isEmpty() && currentCounterparty == null) || payerPartyReference.equals(ourPartyHrefId)) { StandardId proposedCounterparty = StandardId.of(FPML_PARTY_SCHEME, parties.get(receiverPartyReference).get(0)); if (currentCounterparty == null) { tradeInfoBuilder.counterparty(proposedCounterparty); } else if (!currentCounterparty.equals(proposedCounterparty)) { throw new FpmlParseException(Messages.format( "Two different counterparties found: {} and {}", currentCounterparty, proposedCounterparty)); } return PayReceive.PAY; } else if (ourPartyHrefId.isEmpty() || receiverPartyReference.equals(ourPartyHrefId)) { StandardId proposedCounterparty = StandardId.of(FPML_PARTY_SCHEME, parties.get(payerPartyReference).get(0)); if (currentCounterparty == null) { tradeInfoBuilder.counterparty(proposedCounterparty); } else if (!currentCounterparty.equals(proposedCounterparty)) { throw new FpmlParseException(Messages.format( "Two different counterparties found: {} and {}", currentCounterparty, proposedCounterparty)); } return PayReceive.RECEIVE; } else { throw new FpmlParseException(Messages.format( "Neither payerPartyReference nor receiverPartyReference contain our party ID: {}", ourPartyHrefId)); } } //------------------------------------------------------------------------- /** * Converts an FpML 'AdjustedRelativeDateOffset' to a resolved {@code LocalDate}. * * @param baseEl the FpML adjustable date element * @return the resolved date * @throws RuntimeException if unable to parse */ public AdjustableDate parseAdjustedRelativeDateOffset(XmlElement baseEl) { // FpML content: ('periodMultiplier', 'period', 'dayType?', // 'businessDayConvention', 'BusinessCentersOrReference.model?' // 'dateRelativeTo', 'adjustedDate', 'relativeDateAdjustments?') // The 'adjustedDate' element is ignored XmlElement relativeToEl = lookupReference(baseEl.getChild("dateRelativeTo")); LocalDate baseDate; if (relativeToEl.hasContent()) { baseDate = parseDate(relativeToEl); } else if (relativeToEl.getName().contains("relative")) { baseDate = parseAdjustedRelativeDateOffset(relativeToEl).getUnadjusted(); } else { throw new FpmlParseException( "Unable to resolve 'dateRelativeTo' to a date: " + baseEl.getChild("dateRelativeTo").getAttribute(HREF)); } Period period = parsePeriod(baseEl); Optional<XmlElement> dayTypeEl = baseEl.findChild("dayType"); boolean calendarDays = period.isZero() || (dayTypeEl.isPresent() && dayTypeEl.get().getContent().equals("Calendar")); BusinessDayAdjustment bda1 = parseBusinessDayAdjustments(baseEl); BusinessDayAdjustment bda2 = baseEl.findChild("relativeDateAdjustments") .map(el -> parseBusinessDayAdjustments(el)) .orElse(bda1); // interpret and resolve, simple calendar arithmetic or business days LocalDate resolvedDate; if (period.getYears() > 0 || period.getMonths() > 0 || calendarDays) { resolvedDate = bda1.adjust(baseDate.plus(period), refData); } else { LocalDate datePlusBusDays = bda1.getCalendar().resolve(refData).shift(baseDate, period.getDays()); resolvedDate = bda1.adjust(datePlusBusDays, refData); } return AdjustableDate.of(resolvedDate, bda2); } //------------------------------------------------------------------------- /** * Converts an FpML 'RelativeDateOffset' to a {@code DaysAdjustment}. * * @param baseEl the FpML adjustable date element * @return the days adjustment * @throws RuntimeException if unable to parse */ public DaysAdjustment parseRelativeDateOffsetDays(XmlElement baseEl) { // FpML content: ('periodMultiplier', 'period', 'dayType?', // 'businessDayConvention', 'BusinessCentersOrReference.model?' // 'dateRelativeTo', 'adjustedDate') // The 'dateRelativeTo' element is not used here // The 'adjustedDate' element is ignored Period period = parsePeriod(baseEl); if (period.toTotalMonths() != 0) { throw new FpmlParseException("Expected days-based period but found " + period); } Optional<XmlElement> dayTypeEl = baseEl.findChild("dayType"); boolean calendarDays = period.isZero() || (dayTypeEl.isPresent() && dayTypeEl.get().getContent().equals("Calendar")); BusinessDayConvention fixingBdc = convertBusinessDayConvention( baseEl.getChild("businessDayConvention").getContent()); HolidayCalendarId calendar = parseBusinessCenters(baseEl); if (calendarDays) { return DaysAdjustment.ofCalendarDays( period.getDays(), BusinessDayAdjustment.of(fixingBdc, calendar)); } else { return DaysAdjustment.ofBusinessDays(period.getDays(), calendar); } } //------------------------------------------------------------------------- /** * Converts an FpML 'AdjustableDate' to an {@code AdjustableDate}. * * @param baseEl the FpML adjustable date element * @return the adjustable date * @throws RuntimeException if unable to parse */ public AdjustableDate parseAdjustableDate(XmlElement baseEl) { // FpML content: ('unadjustedDate', 'dateAdjustments', 'adjustedDate?') Optional<XmlElement> unadjOptEl = baseEl.findChild("unadjustedDate"); if (unadjOptEl.isPresent()) { LocalDate unadjustedDate = parseDate(unadjOptEl.get()); BusinessDayAdjustment adjustment = parseBusinessDayAdjustments(baseEl.getChild("dateAdjustments")); return AdjustableDate.of(unadjustedDate, adjustment); } LocalDate adjustedDate = parseDate(baseEl.getChild("adjustedDate")); return AdjustableDate.of(adjustedDate); } //------------------------------------------------------------------------- /** * Converts an FpML 'BusinessDayAdjustments' to a {@code BusinessDayAdjustment}. * * @param baseEl the FpML business centers or reference element to parse * @return the business day adjustment * @throws RuntimeException if unable to parse */ public BusinessDayAdjustment parseBusinessDayAdjustments(XmlElement baseEl) { // FpML content: ('businessDayConvention', 'BusinessCentersOrReference.model?') BusinessDayConvention bdc = convertBusinessDayConvention( baseEl.getChild("businessDayConvention").getContent()); Optional<XmlElement> centersEl = baseEl.findChild("businessCenters"); Optional<XmlElement> centersRefEl = baseEl.findChild("businessCentersReference"); HolidayCalendarId calendar = (centersEl.isPresent() || centersRefEl.isPresent() ? parseBusinessCenters(baseEl) : HolidayCalendarIds.NO_HOLIDAYS); return BusinessDayAdjustment.of(bdc, calendar); } //------------------------------------------------------------------------- /** * Converts an FpML 'BusinessCentersOrReference.model' to a {@code HolidayCalendar}. * * @param baseEl the FpML business centers or reference element to parse * @return the holiday calendar * @throws RuntimeException if unable to parse */ public HolidayCalendarId parseBusinessCenters(XmlElement baseEl) { // FpML content: ('businessCentersReference' | 'businessCenters') // FpML 'businessCenters' content: ('businessCenter+') // Each 'businessCenter' is a location treated as a holiday calendar Optional<XmlElement> optionalBusinessCentersEl = baseEl.findChild("businessCenters"); XmlElement businessCentersEl = optionalBusinessCentersEl.orElseGet(() -> lookupReference(baseEl.getChild("businessCentersReference"))); HolidayCalendarId calendar = HolidayCalendarIds.NO_HOLIDAYS; for (XmlElement businessCenterEl : businessCentersEl.getChildren("businessCenter")) { calendar = calendar.combinedWith(parseBusinessCenter(businessCenterEl)); } return calendar; } //------------------------------------------------------------------------- /** * Converts an FpML 'BusinessCenter' to a {@code HolidayCalendar}. * * @param baseEl the FpML calendar element to parse * @return the calendar * @throws RuntimeException if unable to parse */ public HolidayCalendarId parseBusinessCenter(XmlElement baseEl) { validateScheme(baseEl, "businessCenterScheme", "http://www.fpml.org/coding-scheme/business-center"); return convertHolidayCalendar(baseEl.getContent()); } //------------------------------------------------------------------------- /** * Converts an FpML 'FloatingRateIndex.model' to a {@code PriceIndex}. * * @param baseEl the FpML floating rate model element to parse * @return the index * @throws RuntimeException if unable to parse */ public PriceIndex parsePriceIndex(XmlElement baseEl) { XmlElement indexEl = baseEl.getChild("floatingRateIndex"); validateScheme(indexEl, "floatingRateIndexScheme", "http://www.fpml.org/coding-scheme/inflation-index-description"); FloatingRateName floatingName = FloatingRateName.of(indexEl.getContent()); return floatingName.toPriceIndex(); } /** * Converts an FpML 'FloatingRateIndex.model' to an {@code Index}. * * @param baseEl the FpML floating rate model element to parse * @return the index * @throws RuntimeException if unable to parse */ public Index parseIndex(XmlElement baseEl) { List<Index> indexes = parseIndexes(baseEl); if (indexes.size() != 1) { throw new FpmlParseException("Expected one index but found " + indexes.size()); } return indexes.get(0); } /** * Converts an FpML 'FloatingRateIndex' with multiple tenors to an {@code Index}. * * @param baseEl the FpML floating rate index element to parse * @return the index * @throws RuntimeException if unable to parse */ public List<Index> parseIndexes(XmlElement baseEl) { XmlElement indexEl = baseEl.getChild("floatingRateIndex"); validateScheme(indexEl, "floatingRateIndexScheme", "http://www.fpml.org/coding-scheme/floating-rate-index"); FloatingRateName floatingName = FloatingRateName.of(indexEl.getContent()); List<XmlElement> tenorEls = baseEl.getChildren("indexTenor"); if (tenorEls.isEmpty()) { return ImmutableList.of(floatingName.toOvernightIndex()); } else { return tenorEls.stream() .map(el -> floatingName.toIborIndex(parseIndexTenor(el))) .collect(toImmutableList()); } } /** * Converts an FpML 'FloatingRateIndex' tenor to a {@code Tenor}. * * @param baseEl the FpML floating rate index element to parse * @return the period * @throws RuntimeException if unable to parse */ public Tenor parseIndexTenor(XmlElement baseEl) { // FpML content: ('periodMultiplier', 'period') String multiplier = baseEl.getChild("periodMultiplier").getContent(); String unit = baseEl.getChild("period").getContent(); return convertIndexTenor(multiplier, unit); } //------------------------------------------------------------------------- /** * Converts an FpML 'Period' to a {@code Period}. * * @param baseEl the FpML element to parse * @return the period * @throws RuntimeException if unable to parse */ public Period parsePeriod(XmlElement baseEl) { // FpML content: ('periodMultiplier', 'period') String multiplier = baseEl.getChild("periodMultiplier").getContent(); String unit = baseEl.getChild("period").getContent(); return Period.parse("P" + multiplier + unit); } //------------------------------------------------------------------------- /** * Converts an FpML frequency to a {@code Frequency}. * * @param baseEl the FpML element to parse * @return the frequency * @throws RuntimeException if unable to parse */ public Frequency parseFrequency(XmlElement baseEl) { // FpML content: ('periodMultiplier', 'period') String multiplier = baseEl.getChild("periodMultiplier").getContent(); String unit = baseEl.getChild("period").getContent(); if (unit.equals("T")) { return Frequency.TERM; } return convertFrequency(multiplier, unit); } //------------------------------------------------------------------------- /** * Converts an FpML 'Money' to a {@code CurrencyAmount}. * * @param baseEl the FpML money element to parse * @return the currency amount * @throws RuntimeException if unable to parse */ public CurrencyAmount parseCurrencyAmount(XmlElement baseEl) { // FpML content: ('currency', 'amount') Currency currency = parseCurrency(baseEl.getChild("currency")); double amount = parseDecimal(baseEl.getChild("amount")); return CurrencyAmount.of(currency, amount); } //------------------------------------------------------------------------- /** * Converts an FpML 'Currency' to a {@code Currency}. * * @param baseEl the FpML currency element to parse * @return the currency * @throws RuntimeException if unable to parse */ public Currency parseCurrency(XmlElement baseEl) { validateScheme(baseEl, "currencyScheme", "http://www.fpml.org/coding-scheme/external/iso4217"); return Currency.of(baseEl.getContent()); } /** * Converts an FpML 'DayCountFraction' to a {@code DayCount}. * * @param baseEl the FpML day count element to parse * @return the day count * @throws RuntimeException if unable to parse */ public DayCount parseDayCountFraction(XmlElement baseEl) { validateScheme(baseEl, "dayCountFractionScheme", "http://www.fpml.org/coding-scheme/day-count-fraction"); return convertDayCount(baseEl.getContent()); } //------------------------------------------------------------------------- /** * Converts an FpML 'decimal' to a {@code double}. * * @param baseEl the FpML element to parse * @return the double * @throws RuntimeException if unable to parse */ public double parseDecimal(XmlElement baseEl) { return Double.parseDouble(baseEl.getContent()); } /** * Converts an FpML 'date' to a {@code LocalDate}. * * @param baseEl the FpML element to parse * @return the date * @throws RuntimeException if unable to parse */ public LocalDate parseDate(XmlElement baseEl) { return convertDate(baseEl.getContent()); } /** * Converts an FpML 'hourMinuteTime' to a {@code LocalTime}. * * @param baseEl the FpML element to parse * @return the time * @throws RuntimeException if unable to parse */ public LocalTime parseTime(XmlElement baseEl) { return LocalTime.parse(baseEl.getContent()); } //------------------------------------------------------------------------- /** * Converts an FpML day count string to a {@code DayCount}. * * @param fpmlDayCountName the day count name used by FpML * @return the day count * @throws IllegalArgumentException if the day count is not known */ public DayCount convertDayCount(String fpmlDayCountName) { return DayCount.extendedEnum().externalNames(ENUM_FPML).lookup(fpmlDayCountName); } /** * Converts an FpML business day convention string to a {@code BusinessDayConvention}. * * @param fmplBusinessDayConventionName the business day convention name used by FpML * @return the business day convention * @throws IllegalArgumentException if the business day convention is not known */ public BusinessDayConvention convertBusinessDayConvention(String fmplBusinessDayConventionName) { return BusinessDayConvention.extendedEnum().externalNames(ENUM_FPML).lookup(fmplBusinessDayConventionName); } /** * Converts an FpML roll convention string to a {@code RollConvention}. * * @param fmplRollConventionName the roll convention name used by FpML * @return the roll convention * @throws IllegalArgumentException if the roll convention is not known */ public RollConvention convertRollConvention(String fmplRollConventionName) { return RollConvention.extendedEnum().externalNames(ENUM_FPML).lookup(fmplRollConventionName); } /** * Converts an FpML business center string to a {@code HolidayCalendar}. * * @param fpmlBusinessCenter the business center name used by FpML * @return the holiday calendar * @throws IllegalArgumentException if the holiday calendar is not known */ public HolidayCalendarId convertHolidayCalendar(String fpmlBusinessCenter) { return HolidayCalendarId.of(fpmlBusinessCenter); } /** * Converts an FpML frequency string to a {@code Frequency}. * * @param multiplier the multiplier * @param unit the unit * @return the frequency * @throws IllegalArgumentException if the frequency is not known */ public Frequency convertFrequency(String multiplier, String unit) { String periodStr = multiplier + unit; Frequency frequency = FREQUENCY_MAP.get(periodStr); return frequency != null ? frequency : Frequency.parse(periodStr); } /** * Converts an FpML tenor string to a {@code Tenor}. * * @param multiplier the multiplier * @param unit the unit * @return the tenor * @throws IllegalArgumentException if the tenor is not known */ public Tenor convertIndexTenor(String multiplier, String unit) { String periodStr = multiplier + unit; Tenor tenor = TENOR_MAP.get(periodStr); return tenor != null ? tenor : Tenor.parse(periodStr); } /** * Converts an FpML date to a {@code LocalDate}. * * @param dateStr the business center name used by FpML * @return the holiday calendar * @throws DateTimeParseException if the date cannot be parsed */ public LocalDate convertDate(String dateStr) { return LocalDate.parse(dateStr, FPML_DATE_FORMAT); } /** * Returns the {@code ZoneId} matching this string representation of a holiday calendar id. * * @param holidayCalendarId the holiday calendar id string. * @return an optional zone id, an empty optional is returned if no zone id can be found for the holiday calendar id. */ public Optional<ZoneId> getZoneId(String holidayCalendarId) { ZoneId zoneId = HOLIDAY_CALENDARID_MAP.get(holidayCalendarId); if (zoneId == null) { return Optional.empty(); } return Optional.of(zoneId); } //------------------------------------------------------------------------- /** * Validates that a specific element is not present. * * @param baseEl the FpML element to parse * @param elementName the element name * @throws FpmlParseException if the element is found */ public void validateNotPresent(XmlElement baseEl, String elementName) { if (baseEl.findChild(elementName).isPresent()) { throw new FpmlParseException("Unsupported element: '" + elementName + "'"); } } /** * Validates that the scheme attribute is known. * * @param baseEl the FpML element to parse * @param schemeAttr the scheme attribute name * @param schemeValue the scheme attribute value * @throws FpmlParseException if the scheme does not match */ public void validateScheme(XmlElement baseEl, String schemeAttr, String schemeValue) { if (baseEl.getAttributes().containsKey(schemeAttr)) { String scheme = baseEl.getAttribute(schemeAttr); if (!scheme.startsWith(schemeValue)) { throw new FpmlParseException("Unknown '" + schemeAttr + "' attribute value: " + scheme); } } } //------------------------------------------------------------------------- /** * Looks up an element by href/id reference. * * @param hrefEl the element containing the href/id * @return the matched element * @throws FpmlParseException if the reference is not found */ // lookup an element via href/id reference public XmlElement lookupReference(XmlElement hrefEl) { String hrefId = hrefEl.getAttribute(HREF); XmlElement el = references.get(hrefId); if (el == null) { throw new FpmlParseException(Messages.format("Document reference not found: href='{}'", hrefId)); } return el; } }