/** * Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.web.analytics.blotter; import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.joda.beans.BeanBuilder; import org.joda.beans.MetaBean; import org.joda.beans.MetaProperty; import org.joda.beans.Property; import org.joda.convert.StringConvert; import org.threeten.bp.ZonedDateTime; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.opengamma.core.security.Security; import com.opengamma.core.security.SecurityLink; import com.opengamma.financial.security.FinancialSecurity; import com.opengamma.id.ExternalId; import com.opengamma.id.ExternalIdBundle; import com.opengamma.id.UniqueId; import com.opengamma.id.VersionCorrection; import com.opengamma.master.portfolio.ManageablePortfolio; import com.opengamma.master.portfolio.ManageablePortfolioNode; import com.opengamma.master.portfolio.PortfolioDocument; import com.opengamma.master.portfolio.PortfolioMaster; import com.opengamma.master.portfolio.PortfolioSearchRequest; import com.opengamma.master.portfolio.PortfolioSearchResult; import com.opengamma.master.position.ManageablePosition; import com.opengamma.master.position.ManageableTrade; import com.opengamma.master.position.PositionDocument; import com.opengamma.master.position.PositionMaster; import com.opengamma.master.security.ManageableSecurity; import com.opengamma.master.security.ManageableSecurityLink; import com.opengamma.master.security.SecurityDocument; import com.opengamma.master.security.SecurityMaster; import com.opengamma.master.security.SecuritySearchRequest; import com.opengamma.master.security.SecuritySearchResult; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.OpenGammaClock; /** * Builds and saves trades, securities and underlying securities for OTC securities. * TODO the use of VersionCorrection.LATEST in this class is incorrect * the weak link between trades, positions and securities is a problem for OTCs because in reality they're a single * atomic object. the problem at the moment is there's no way to know which version of the security is being modified * given the unique ID of the trade. therefore it's not possible to detect any concurrent modification of securities, * the last update wins. there are various potential fixes but it might not be worth doing before the imminent refactor * of trades, positions and securities. */ /* package */ class OtcTradeBuilder extends AbstractTradeBuilder { /** Type name for OTC trades used in the data sent to the client. */ /* package */ static final String TRADE_TYPE_NAME = "OtcTrade"; /* package */ OtcTradeBuilder(PositionMaster positionMaster, PortfolioMaster portfoioMaster, SecurityMaster securityMaster, Set<MetaBean> metaBeans, StringConvert stringConvert) { super(positionMaster, portfoioMaster, securityMaster, metaBeans, stringConvert); } @SuppressWarnings("deprecation") UniqueId addTrade(BeanDataSource tradeData, BeanDataSource securityData, BeanDataSource underlyingData, UniqueId nodeId) { /* validate: underlying is present underlying type is correct */ ManageableSecurity underlying = buildUnderlying(underlyingData); ManageableSecurity security; if (underlying == null) { security = BlotterUtils.buildSecurity(securityData); } else { ManageableSecurity savedUnderlying = getSecurityMaster().add(new SecurityDocument(underlying)).getSecurity(); security = buildSecurity(securityData, savedUnderlying); } ManageableSecurity savedSecurity = getSecurityMaster().add(new SecurityDocument(security)).getSecurity(); ManageableTrade trade = buildTrade(tradeData); trade.setSecurityLink(new ManageableSecurityLink(savedSecurity.getUniqueId())); ManageablePosition position = new ManageablePosition(); position.setQuantity(BigDecimal.ONE); position.setSecurityLink(new ManageableSecurityLink(trade.getSecurityLink())); position.setTrades(Lists.newArrayList(trade)); ManageablePosition savedPosition = getPositionMaster().add(new PositionDocument(position)).getPosition(); ManageableTrade savedTrade = savedPosition.getTrades().get(0); PortfolioSearchRequest searchRequest = new PortfolioSearchRequest(); searchRequest.addNodeObjectId(nodeId.getObjectId()); PortfolioSearchResult searchResult = getPortfolioMaster().search(searchRequest); ManageablePortfolio portfolio = searchResult.getSinglePortfolio(); ManageablePortfolioNode node = findNode(portfolio, nodeId); node.addPosition(savedPosition.getUniqueId()); getPortfolioMaster().update(new PortfolioDocument(portfolio)); return savedTrade.getUniqueId(); } /* package */ UniqueId updatePosition(UniqueId positionId, BeanDataSource tradeData, BeanDataSource securityData, BeanDataSource underlyingData) { ManageableTrade trade = buildTrade(tradeData); ManageablePosition position = getPositionMaster().get(positionId).getPosition(); ManageableSecurity previousSecurity = loadSecurity(position.getSecurityLink()); return updateSecuritiesAndPosition(securityData, underlyingData, trade, previousSecurity, positionId); } /* package */ UniqueId updateTrade(BeanDataSource tradeData, BeanDataSource securityData, BeanDataSource underlyingData) { if (!TRADE_TYPE_NAME.equals(tradeData.getBeanTypeName())) { throw new IllegalArgumentException("Can only build trades of type " + TRADE_TYPE_NAME + ", type name = " + tradeData.getBeanTypeName()); } /* validate: underlying is present underlying type is correct security type hasn't changed trade ID is versioned */ ManageableTrade trade = buildTrade(tradeData); ManageableTrade previousTrade = getPositionMaster().getTrade(trade.getUniqueId()); ManageableSecurity previousSecurity = loadSecurity(previousTrade.getSecurityLink()); UniqueId previousPositionId = previousTrade.getParentPositionId(); return updateSecuritiesAndPosition(securityData, underlyingData, trade, previousSecurity, previousPositionId); } private ManageableSecurity loadSecurity(SecurityLink securityLink) { if (securityLink.getObjectId() != null) { return getSecurityMaster().get(securityLink.getObjectId(), VersionCorrection.LATEST).getSecurity(); } else if (securityLink.getExternalId() != null) { ExternalIdBundle idBundle = securityLink.getExternalId(); SecuritySearchResult searchResult = getSecurityMaster().search(new SecuritySearchRequest(idBundle)); if (searchResult.getSecurities().isEmpty()) { throw new IllegalArgumentException("No security found for ID bundle " + idBundle); } return searchResult.getFirstSecurity(); } else { throw new IllegalArgumentException("No IDs in security link " + securityLink); } } @SuppressWarnings("deprecation") private UniqueId updateSecuritiesAndPosition(BeanDataSource securityData, BeanDataSource underlyingData, ManageableTrade trade, ManageableSecurity previousSecurity, UniqueId positionId) { // need the previous underlying so we don't lose the ID bundle, the data doesn't contain it ExternalIdBundle previousUnderlyingIdBundle; if (previousSecurity instanceof FinancialSecurity) { UnderlyingSecurityVisitor visitor = new UnderlyingSecurityVisitor(VersionCorrection.LATEST, getSecurityMaster()); ManageableSecurity previousUnderlying = ((FinancialSecurity) previousSecurity).accept(visitor); if (previousUnderlying != null) { previousUnderlyingIdBundle = previousUnderlying.getExternalIdBundle(); } else { previousUnderlyingIdBundle = ExternalIdBundle.EMPTY; } } else { previousUnderlyingIdBundle = ExternalIdBundle.EMPTY; } ManageableSecurity underlying = buildUnderlying(underlyingData, previousUnderlyingIdBundle); ManageableSecurity security; if (underlying == null) { security = BlotterUtils.buildSecurity(securityData, previousSecurity.getExternalIdBundle()); } else { // need to set the unique ID to the ID from the previous version, securities aren't allowed to change // any changes in the security data are interpreted as edits to the security ManageableSecurity previousUnderlying = getUnderlyingSecurity(previousSecurity, VersionCorrection.LATEST); validateSecurity(underlying, previousUnderlying); underlying.setUniqueId(previousUnderlying.getUniqueId()); ManageableSecurity savedUnderlying = getSecurityMaster().update(new SecurityDocument(underlying)).getSecurity(); security = buildSecurity(securityData, savedUnderlying, previousSecurity.getExternalIdBundle()); } // need to set the unique ID to the ID from the previous version, securities aren't allowed to change // any changes in the security data are interpreted as edits to the security validateSecurity(security, previousSecurity); security.setUniqueId(previousSecurity.getUniqueId()); ManageableSecurity savedSecurity = getSecurityMaster().update(new SecurityDocument(security)).getSecurity(); trade.setSecurityLink(new ManageableSecurityLink(savedSecurity.getUniqueId())); ManageablePosition position = getPositionMaster().get(positionId).getPosition(); position.setTrades(Lists.newArrayList(trade)); ManageablePosition savedPosition = getPositionMaster().update(new PositionDocument(position)).getPosition(); ManageableTrade savedTrade = savedPosition.getTrades().get(0); return savedTrade.getUniqueId(); } private ManageableSecurity getUnderlyingSecurity(ManageableSecurity security, VersionCorrection versionCorrection) { if (security instanceof FinancialSecurity) { UnderlyingSecurityVisitor visitor = new UnderlyingSecurityVisitor(versionCorrection, getSecurityMaster()); return ((FinancialSecurity) security).accept(visitor); } else { return null; } } /** * Checks that the new and old versions of a security have the same type and if the new version specifies an ID * it is the same as the old ID. * @param newVersion The new version of the security * @param previousVersion The previous version of the security */ private static void validateSecurity(ManageableSecurity newVersion, ManageableSecurity previousVersion) { if (!newVersion.getClass().equals(previousVersion.getClass())) { throw new IllegalArgumentException("Security type cannot change, new version " + newVersion + ", " + "previousVersion: " + previousVersion); } // TODO this should check for equality between the IDs but that's not working ATM // needs to be part of the bigger fix for the problem caused by the weak links between the different parts // of OTC trades if (newVersion.getUniqueId() != null && !newVersion.getUniqueId().equalObjectId(previousVersion.getUniqueId())) { throw new IllegalArgumentException("Cannot update a security with a different ID, " + "new ID: " + newVersion.getUniqueId() + ", " + "previous ID: " + previousVersion.getUniqueId()); } } private ManageableTrade buildTrade(BeanDataSource tradeData) { ManageableTrade.Meta meta = ManageableTrade.meta(); BeanBuilder<? extends ManageableTrade> tradeBuilder = tradeBuilder(tradeData, meta.uniqueId(), meta.tradeDate(), meta.tradeTime(), meta.premium(), meta.premiumCurrency(), meta.premiumDate(), meta.premiumTime()); tradeBuilder.set(meta.attributes(), tradeData.getMapValues(meta.attributes().name())); tradeBuilder.set(meta.quantity(), BigDecimal.ONE); // the link needs to be non-null but the real ID can't be set until the security has been created later tradeBuilder.set(meta.securityLink(), new ManageableSecurityLink()); String counterparty = (String) tradeData.getValue(COUNTERPARTY); if (StringUtils.isEmpty(counterparty)) { counterparty = DEFAULT_COUNTERPARTY; } tradeBuilder.set(meta.counterpartyExternalId(), ExternalId.of(CPTY_SCHEME, counterparty)); return tradeBuilder.build(); } // TODO move these to a separate class that only extracts data, also handles securities and underlyings /** * Extracts trade data and populates a data sink. * @param trade The trade * @param sink The sink that should be populated with the trade data */ /* package */ void extractTradeData(ManageableTrade trade, BeanDataSink<?> sink) { sink.setValue("type", TRADE_TYPE_NAME); extractPropertyData(trade.uniqueId(), sink); extractPropertyData(trade.tradeDate(), sink); extractPropertyData(trade.tradeTime(), sink); extractPropertyData(trade.premium(), sink); extractPropertyData(trade.premiumCurrency(), sink); extractPropertyData(trade.premiumDate(), sink); extractPropertyData(trade.premiumTime(), sink); sink.setMap(trade.attributes().name(), trade.getAttributes()); // this shouldn't be necessary as counterparty ID isn't nullable but there's a bug in the implementation of // ManageableTrade which allows null values ExternalId counterpartyId = trade.getCounterpartyExternalId(); String counterpartyValue; if (counterpartyId != null) { counterpartyValue = counterpartyId.getValue(); } else { counterpartyValue = null; } sink.setValue(COUNTERPARTY, counterpartyValue); } private void extractPropertyData(Property<?> property, BeanDataSink<?> sink) { sink.setValue(property.name(), getStringConvert().convertToString(property.metaProperty().get(property.bean()))); } /** * Creates a builder for a {@link ManageableTrade} and sets the simple properties from the data source. * @param tradeData The trade data * @param properties The trade properties to set * @return A builder with property values set from the trade data */ private BeanBuilder<? extends ManageableTrade> tradeBuilder(BeanDataSource tradeData, MetaProperty<?>... properties) { BeanBuilder<? extends ManageableTrade> builder = ManageableTrade.meta().builder(); for (MetaProperty<?> property : properties) { builder.set(property, getStringConvert().convertFromString(property.propertyType(), (String) tradeData.getValue(property.name()))); } return builder; } private FinancialSecurity buildSecurity(BeanDataSource securityData, Security underlying) { return buildSecurity(securityData, underlying, ExternalIdBundle.EMPTY); } private FinancialSecurity buildSecurity(BeanDataSource securityData, Security underlying, ExternalIdBundle idBundle) { ArgumentChecker.notNull(underlying, "underlying"); BeanDataSource dataSource; ExternalId underlyingId = getUnderlyingId(underlying); // TODO would it be better to just return the bean builder from the visitor and handle this property manually? // TODO would have to use a different property for every security with underlyingId, there's no common supertype with it if (underlyingId == null) { throw new IllegalArgumentException("Unable to get underlying ID for security " + underlying); } // TODO I'm not keen on this, it doesn't smell great dataSource = new PropertyReplacingDataSource(securityData, "underlyingId", underlyingId.toString()); return BlotterUtils.buildSecurity(dataSource, idBundle); } private FinancialSecurity buildUnderlying(BeanDataSource underlyingData) { return buildUnderlying(underlyingData, ExternalIdBundle.EMPTY); } private FinancialSecurity buildUnderlying(BeanDataSource underlyingData, ExternalIdBundle idBundle) { if (underlyingData == null) { return null; } return BlotterUtils.buildSecurity(underlyingData, idBundle); } private ExternalId getUnderlyingId(Security underlying) { ExternalId underlyingId; if (underlying instanceof FinancialSecurity) { underlyingId = ((FinancialSecurity) underlying).accept(new ExternalIdVisitor(getSecurityMaster())); } else { underlyingId = null; } return underlyingId; } // TODO different versions for OTC / non OTC // the horror... make this go away TODO move to the TradeBuilers? they create the trades /* package */ static Map<String, Object> tradeStructure() { Map<String, Object> structure = Maps.newHashMap(); List<Map<String, Object>> properties = Lists.newArrayList(); properties.add(property("uniqueId", true, true, typeInfo("string", "UniqueId"))); properties.add(property("counterparty", false, false, typeInfo("string", ""))); properties.add(property("tradeDate", true, false, typeInfo("string", "LocalDate"))); properties.add(property("tradeTime", true, false, typeInfo("string", "OffsetTime"))); properties.add(property("premium", true, false, typeInfo("number", ""))); properties.add(property("premiumCurrency", true, false, typeInfo("string", "Currency"))); properties.add(property("premiumDate", true, false, typeInfo("string", "LocalDate"))); properties.add(property("premiumTime", true, false, typeInfo("string", "OffsetTime"))); properties.add(attributesProperty()); structure.put("type", TRADE_TYPE_NAME); structure.put("properties", properties); structure.put("now", ZonedDateTime.now(OpenGammaClock.getInstance())); return structure; } }