/**
* Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.web.analytics.blotter;
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.joda.convert.StringConverter;
import org.threeten.bp.ZonedDateTime;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.opengamma.DataNotFoundException;
import com.opengamma.core.security.Security;
import com.opengamma.id.ExternalId;
import com.opengamma.id.ExternalIdBundle;
import com.opengamma.id.ObjectId;
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.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.ManageableSecurityLink;
import com.opengamma.master.security.SecurityMaster;
import com.opengamma.master.security.impl.MasterSecuritySource;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.OpenGammaClock;
/**
*
*/
/* package */ class FungibleTradeBuilder extends AbstractTradeBuilder {
/** Value for the type name in the JSON for fungible trades */
/* package */ static final String TRADE_TYPE_NAME = "FungibleTrade";
/** Key used in the JSON for the security ID bundle. */
private static final String SECURITY_ID_BUNDLE = "securityIdBundle";
/** For loading and saving portfolios and nodes. */
private final PortfolioMaster _portfolioMaster;
/** For loading and saving securities. */
private final MasterSecuritySource _securitySource;
/* package */ FungibleTradeBuilder(PositionMaster positionMaster,
PortfolioMaster portfolioMaster,
SecurityMaster securityMaster,
Set<MetaBean> metaBeans,
StringConvert stringConvert) {
super(positionMaster, portfolioMaster, securityMaster, metaBeans, stringConvert);
ArgumentChecker.notNull(portfolioMaster, "portfolioMaster");
_portfolioMaster = portfolioMaster;
_securitySource = new MasterSecuritySource(getSecurityMaster());
}
/**
* TODO make this non-static and make stringConvert a field
* 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, StringConvert stringConvert) {
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);
extractPropertyData(trade.quantity(), 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);
ExternalIdBundle securityIdBundle = trade.getSecurityLink().getExternalId();
StringConverter<ExternalIdBundle> converter = stringConvert.findConverter(ExternalIdBundle.class);
sink.setValue(SECURITY_ID_BUNDLE, converter.convertToString(securityIdBundle));
}
private void extractPropertyData(Property<?> property, BeanDataSink<?> sink) {
sink.setValue(property.name(), getStringConvert().convertToString(property.metaProperty().get(property.bean())));
}
/* package */ UniqueId addTrade(BeanDataSource tradeData, UniqueId nodeId) {
ManageableTrade trade = buildTrade(tradeData);
Security security = trade.getSecurityLink().resolve(_securitySource);
// this slightly awkward approach is due to the portfolio master API. you can look up a node directly but in order
// to save it you have to save the whole portfolio. this means you need to look up the node to find the portfolio
// ID, look up the portfolio, find the node in the portfolio, modify that copy of the node and save the portfolio
// TODO can use a portfolio search request and only hit the master once
ManageablePortfolioNode node = _portfolioMaster.getNode(nodeId);
ManageablePortfolio portfolio = _portfolioMaster.get(node.getPortfolioId()).getPortfolio();
ManageablePortfolioNode portfolioNode = findNode(portfolio, nodeId);
ManageablePosition position = findPosition(portfolioNode, security);
if (position == null) {
// no position in this security on the node, create a new position just for this trade
ManageablePosition newPosition = new ManageablePosition(trade.getQuantity(), security.getExternalIdBundle());
newPosition.addTrade(trade);
ManageablePosition savedPosition = getPositionMaster().add(new PositionDocument(newPosition)).getPosition();
portfolioNode.addPosition(savedPosition.getUniqueId());
_portfolioMaster.update(new PortfolioDocument(portfolio));
return savedPosition.getTrades().get(0).getUniqueId();
} else {
position.addTrade(trade);
position.setQuantity(position.getQuantity().add(trade.getQuantity()));
ManageablePosition savedPosition = getPositionMaster().update(new PositionDocument(position)).getPosition();
List<ManageableTrade> savedTrades = savedPosition.getTrades();
return savedTrades.get(savedTrades.size() - 1).getUniqueId();
}
}
/**
* Updates a position directly. This is only allowed for positions with no trades. The position's size is changed
* to match the quantity in the trade details and a single trade is created for the position.
* @param tradeData Trade data for the position
* @param positionId Unique ID of the position
*/
/* package */ void updatePosition(BeanDataSource tradeData, UniqueId positionId) {
ManageableTrade trade = buildTrade(tradeData);
// TODO check if the ID is versioned?
ManageablePosition position = getPositionMaster().get(positionId).getPosition();
// TODO this is a temporary workaround for a client bug. not sure what the correct behaviour is yet
trade.setUniqueId(null);
/*if (!trade.getSecurityLink().equals(position.getSecurityLink())) {
throw new IllegalArgumentException("Cannot update a position's security. new version " + trade.getSecurityLink() +
", previous version: " + position.getSecurityLink());
}*/
if (position.getTrades().size() != 0) {
throw new IllegalArgumentException("Cannot directly update a position that contains trade. Update the trades");
}
position.setTrades(Lists.newArrayList(trade));
position.setQuantity(trade.getQuantity());
getPositionMaster().update(new PositionDocument(position)).getPosition();
}
// TODO would it make more sense to have a void return type? does the client use the returned ID?
/* package */ UniqueId updateTrade(BeanDataSource tradeData) {
ManageableTrade trade = buildTrade(tradeData);
ManageableTrade previousTrade = getPositionMaster().getTrade(trade.getUniqueId());
ManageablePosition position = getPositionMaster().get(previousTrade.getParentPositionId()).getPosition();
if (!trade.getSecurityLink().equals(previousTrade.getSecurityLink())) {
throw new IllegalArgumentException("Cannot update a trade's security. new version " + trade +
", previous version: " + previousTrade);
}
List<ManageableTrade> trades = Lists.newArrayList();
for (ManageableTrade existingTrade : position.getTrades()) {
if (existingTrade.getUniqueId().equals(trade.getUniqueId())) {
trades.add(trade);
position.setQuantity(position.getQuantity().subtract(existingTrade.getQuantity()).add(trade.getQuantity()));
} else {
trades.add(existingTrade);
}
}
position.setTrades(trades);
ManageablePosition savedPosition = getPositionMaster().update(new PositionDocument(position)).getPosition();
ManageableTrade savedTrade = savedPosition.getTrade(trade.getUniqueId().getObjectId());
if (savedTrade == null) {
// shouldn't ever happen
throw new DataNotFoundException("Failed to save trade " + trade + " to position " + savedPosition);
} else {
return savedTrade.getUniqueId();
}
}
/**
* Returns a position from a node in a security or null if there isn't one.
* @param node A portfolio node
* @param security The security
* @return A position from the node in the security or null if there isn't one
*/
private ManageablePosition findPosition(ManageablePortfolioNode node, Security security) {
for (ObjectId positionId : node.getPositionIds()) {
// TODO which version do I want? will LATEST do?
PositionDocument document = getPositionMaster().get(positionId, VersionCorrection.LATEST);
ManageablePosition position = document.getPosition();
Security positionSecurity = position.getSecurityLink().resolve(_securitySource);
if (positionSecurity.getExternalIdBundle().containsAny(security.getExternalIdBundle())) {
return position;
}
}
return null;
}
private ManageableTrade buildTrade(BeanDataSource tradeData) {
if (!TRADE_TYPE_NAME.equals(tradeData.getBeanTypeName())) {
throw new IllegalArgumentException("Can only build trades of type " + TRADE_TYPE_NAME +
", type name = " + tradeData.getBeanTypeName());
}
ManageableTrade.Meta meta = ManageableTrade.meta();
BeanBuilder<? extends ManageableTrade> tradeBuilder =
tradeBuilder(tradeData,
meta.uniqueId(),
meta.tradeDate(),
meta.tradeTime(),
meta.premium(),
meta.premiumCurrency(),
meta.premiumDate(),
meta.quantity(),
meta.premiumTime());
tradeBuilder.set(meta.attributes(), tradeData.getMapValues(meta.attributes().name()));
String idBundleStr = (String) tradeData.getValue(SECURITY_ID_BUNDLE);
// TODO check the security exists and load it if not? and the underlying?
ExternalIdBundle securityIdBundle = getStringConvert().convertFromString(ExternalIdBundle.class, idBundleStr);
tradeBuilder.set(meta.securityLink(), new ManageableSecurityLink(securityIdBundle));
// this property is done manually so the client can just provide the counterparty name but the counterparty
// on the trade is an external ID with the standard counterparty scheme
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();
}
/**
* 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;
}
// TODO different versions for OTC / non OTC
// the horror... make this go away
/* 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("quantity", true, false, typeInfo("number", "")));
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(property("securityIdBundle", true, false, typeInfo("string", "ExternalIdBundle")));
properties.add(attributesProperty());
structure.put("type", TRADE_TYPE_NAME);
structure.put("properties", properties);
structure.put("now", ZonedDateTime.now(OpenGammaClock.getInstance()));
return structure;
}
}