/**
* 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.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang.StringUtils;
import org.joda.beans.Bean;
import org.joda.beans.JodaBeanUtils;
import org.joda.beans.MetaBean;
import org.json.JSONException;
import org.json.JSONObject;
import org.threeten.bp.LocalDate;
import org.threeten.bp.ZonedDateTime;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.opengamma.DataNotFoundException;
import com.opengamma.OpenGammaRuntimeException;
import com.opengamma.analytics.financial.credit.DebtSeniority;
import com.opengamma.analytics.financial.credit.RestructuringClause;
import com.opengamma.core.security.Security;
import com.opengamma.financial.convention.StubType;
import com.opengamma.financial.convention.businessday.BusinessDayConvention;
import com.opengamma.financial.convention.daycount.DayCount;
import com.opengamma.financial.convention.frequency.Frequency;
import com.opengamma.financial.security.FinancialSecurity;
import com.opengamma.financial.security.LongShort;
import com.opengamma.financial.security.cds.AbstractCreditDefaultSwapSecurity;
import com.opengamma.financial.security.future.InterestRateFutureSecurity;
import com.opengamma.financial.security.option.BarrierDirection;
import com.opengamma.financial.security.option.BarrierType;
import com.opengamma.financial.security.option.CreditDefaultSwapOptionSecurity;
import com.opengamma.financial.security.option.ExerciseType;
import com.opengamma.financial.security.option.IRFutureOptionSecurity;
import com.opengamma.financial.security.option.MonitoringType;
import com.opengamma.financial.security.option.SamplingFrequency;
import com.opengamma.financial.security.option.SwaptionSecurity;
import com.opengamma.financial.security.swap.FloatingRateType;
import com.opengamma.financial.security.swap.InterpolationMethod;
import com.opengamma.financial.security.swap.SwapSecurity;
import com.opengamma.id.ExternalId;
import com.opengamma.id.UniqueId;
import com.opengamma.id.VersionCorrection;
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.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;
import com.opengamma.web.FreemarkerOutputter;
import com.opengamma.web.analytics.OtcSecurityVisitor;
/**
* REST resource for the trade blotter and trade entry forms.
*/
@Path("blotter")
public class BlotterResource {
/** Map of underlying security types keyed by the owning security type. */
private static final Map<Class<?>, Class<?>> s_underlyingSecurityTypes = ImmutableMap.<Class<?>, Class<?>>of(
IRFutureOptionSecurity.class, InterestRateFutureSecurity.class,
SwaptionSecurity.class, SwapSecurity.class,
CreditDefaultSwapOptionSecurity.class, AbstractCreditDefaultSwapSecurity.class);
/** Map of paths to the endpoints for looking up values, keyed by the value class. */
private static final Map<Class<?>, String> s_endpoints = Maps.newHashMap();
/** OTC Security types that can be created by the trade entry froms. */
private static final List<String> s_securityTypeNames = Lists.newArrayList();
/** Types that can be created by the trade entry forms that aren't securities by are required by them (e.g. legs). */
private static final List<String> s_otherTypeNames = Lists.newArrayList();
/** Meta beans for types that can be created by the trade entry forms keyed by type name. */
private static final Map<String, MetaBean> s_metaBeansByTypeName = Maps.newHashMap();
static {
s_endpoints.put(Frequency.class, "frequencies");
s_endpoints.put(ExerciseType.class, "exercisetypes");
s_endpoints.put(DayCount.class, "daycountconventions");
s_endpoints.put(BusinessDayConvention.class, "businessdayconventions");
s_endpoints.put(BarrierType.class, "barriertypes");
s_endpoints.put(BarrierDirection.class, "barrierdirections");
s_endpoints.put(SamplingFrequency.class, "samplingfrequencies");
s_endpoints.put(FloatingRateType.class, "floatingratetypes");
s_endpoints.put(LongShort.class, "longshort");
s_endpoints.put(MonitoringType.class, "monitoringtype");
s_endpoints.put(DebtSeniority.class, "debtseniority");
s_endpoints.put(RestructuringClause.class, "restructuringclause");
s_endpoints.put(StubType.class, "stubtype");
s_endpoints.put(InterpolationMethod.class, "interpolationmethods");
for (MetaBean metaBean : BlotterUtils.getMetaBeans()) {
Class<? extends Bean> beanType = metaBean.beanType();
String typeName = beanType.getSimpleName();
s_metaBeansByTypeName.put(typeName, metaBean);
if (isSecurity(beanType)) {
s_securityTypeNames.add(typeName);
} else {
s_otherTypeNames.add(typeName);
}
}
s_otherTypeNames.add(OtcTradeBuilder.TRADE_TYPE_NAME);
s_otherTypeNames.add(FungibleTradeBuilder.TRADE_TYPE_NAME);
Collections.sort(s_otherTypeNames);
Collections.sort(s_securityTypeNames);
}
/** For loading and saving securities. */
private final SecurityMaster _securityMaster;
/** For loading and saving positions. */
private final PositionMaster _positionMaster;
/** For creating output from Freemarker templates. */
private FreemarkerOutputter _freemarker;
/** For building and saving new trades and associated securities. */
private final OtcTradeBuilder _otcTradeBuilder;
/** For building trades in fungible securities. */
private final FungibleTradeBuilder _fungibleTradeBuilder;
/** For removing positions. */
private final PortfolioMaster _portfolioMaster;
public BlotterResource(SecurityMaster securityMaster, PortfolioMaster portfolioMaster, PositionMaster positionMaster) {
ArgumentChecker.notNull(securityMaster, "securityMaster");
ArgumentChecker.notNull(portfolioMaster, "portfolioMaster");
ArgumentChecker.notNull(positionMaster, "positionMaster");
_securityMaster = securityMaster;
_positionMaster = positionMaster;
_portfolioMaster = portfolioMaster;
_fungibleTradeBuilder = new FungibleTradeBuilder(_positionMaster,
_portfolioMaster,
_securityMaster,
BlotterUtils.getMetaBeans(),
BlotterUtils.getStringConvert());
_otcTradeBuilder = new OtcTradeBuilder(_positionMaster,
_portfolioMaster,
_securityMaster,
BlotterUtils.getMetaBeans(),
BlotterUtils.getStringConvert());
}
/* package */ static boolean isSecurity(Class<?> type) {
if (type == null) {
return false;
} else if (ManageableSecurity.class.equals(type)) {
return true;
} else {
return isSecurity(type.getSuperclass());
}
}
@Context
public void setServletContext(final ServletContext servletContext) {
_freemarker = new FreemarkerOutputter(servletContext);
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("types")
public String getTypes() {
Map<Object, Object> data = map("title", "Types",
"securityTypeNames", s_securityTypeNames,
"otherTypeNames", s_otherTypeNames);
return _freemarker.build("blotter/bean-types.ftl", data);
}
@SuppressWarnings("unchecked")
@GET
@Produces(MediaType.TEXT_HTML)
@Path("types/{typeName}")
public String getStructure(@PathParam("typeName") String typeName) {
Map<String, Object> beanData;
// TODO tell don't ask
if (typeName.equals(OtcTradeBuilder.TRADE_TYPE_NAME)) {
beanData = OtcTradeBuilder.tradeStructure();
} else if (typeName.equals(FungibleTradeBuilder.TRADE_TYPE_NAME)) {
beanData = FungibleTradeBuilder.tradeStructure();
} else {
MetaBean metaBean = s_metaBeansByTypeName.get(typeName);
if (metaBean == null) {
throw new DataNotFoundException("Unknown type name " + typeName);
}
BeanStructureBuilder structureBuilder = new BeanStructureBuilder(BlotterUtils.getMetaBeans(),
s_underlyingSecurityTypes,
s_endpoints,
BlotterUtils.getStringConvert());
BeanTraverser traverser = BlotterUtils.structureBuildingTraverser();
beanData = (Map<String, Object>) traverser.traverse(metaBean, structureBuilder);
}
return _freemarker.build("blotter/bean-structure.ftl", beanData);
}
// for getting the security for fungible trades - the user can change the ID and see the trade details update
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("securities/{securityExternalId}")
public String getSecurityJSON(@PathParam("securityExternalId") String securityExternalIdStr) {
if (StringUtils.isEmpty(securityExternalIdStr)) {
return new JSONObject().toString();
}
ExternalId securityExternalId = ExternalId.parse(securityExternalIdStr);
SecuritySearchResult searchResult = _securityMaster.search(new SecuritySearchRequest(securityExternalId));
if (searchResult.getSecurities().size() == 0) {
throw new DataNotFoundException("No security found with ID " + securityExternalId);
}
ManageableSecurity security = searchResult.getFirstSecurity();
BeanVisitor<JSONObject> securityVisitor =
new BuildingBeanVisitor<>(security, new JsonDataSink(BlotterUtils.getJsonBuildingConverters()));
BeanTraverser securityTraverser = BlotterUtils.securityJsonBuildingTraverser();
MetaBean securityMetaBean = JodaBeanUtils.metaBean(security.getClass());
JSONObject securityJson = (JSONObject) securityTraverser.traverse(securityMetaBean, securityVisitor);
return securityJson.toString();
}
// for getting the trade and security data for an OTC trade/security/position
// TODO move this logic to trade builder? can it populate a BeanDataSink to build the JSON?
// TODO refactor this, it's ugly. can the security and trade logic be cleanly moved into classes for OTC & fungible?
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("trades/{tradeId}")
public String getTradeJSON(@PathParam("tradeId") String tradeIdStr) {
UniqueId tradeId = UniqueId.parse(tradeIdStr);
if (!tradeId.isLatest()) {
throw new IllegalArgumentException("The blotter can only be used to update the latest version of a trade");
}
ManageableTrade trade = _positionMaster.getTrade(tradeId);
ManageableSecurityLink securityLink = trade.getSecurityLink();
return buildTradeJSON(trade, securityLink);
}
// TODO does it matter if the trade is not the most recent?
@DELETE
@Produces(MediaType.APPLICATION_JSON)
@Path("trades/{tradeId}")
public Response deleteTrade(@PathParam("tradeId") String tradeIdStr) {
UniqueId tradeId = UniqueId.parse(tradeIdStr);
ManageableTrade trade = _positionMaster.getTrade(tradeId);
PositionDocument positionDoc = _positionMaster.get(trade.getParentPositionId());
positionDoc.getPosition().removeTrade(trade);
_positionMaster.update(positionDoc);
return Response.ok().build();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("positions/{positionId}")
public String getPositionJSON(@PathParam("positionId") String positionIdStr) {
UniqueId positionId = UniqueId.parse(positionIdStr);
if (!positionId.isLatest()) {
throw new IllegalArgumentException("The blotter can only be used to update the latest version of a trade");
}
ManageablePosition position = _positionMaster.get(positionId).getPosition();
ManageableSecurityLink securityLink = position.getSecurityLink();
ManageableTrade trade = new ManageableTrade();
trade.setSecurityLink(securityLink);
if (position.getTrades().size() == 0) {
trade.setTradeDate(LocalDate.now());
trade.setCounterpartyExternalId(ExternalId.of(AbstractTradeBuilder.CPTY_SCHEME, AbstractTradeBuilder.DEFAULT_COUNTERPARTY));
trade.setQuantity(position.getQuantity());
}
return buildTradeJSON(trade, securityLink);
}
private String buildTradeJSON(ManageableTrade trade, ManageableSecurityLink securityLink) {
ManageableSecurity security = findSecurity(securityLink);
JSONObject root = new JSONObject();
try {
JsonDataSink tradeSink = new JsonDataSink(BlotterUtils.getJsonBuildingConverters());
if (isOtc(security)) {
_otcTradeBuilder.extractTradeData(trade, tradeSink);
MetaBean securityMetaBean = s_metaBeansByTypeName.get(security.getClass().getSimpleName());
if (securityMetaBean == null) {
throw new DataNotFoundException("No MetaBean is registered for security type " + security.getClass().getName());
}
BeanVisitor<JSONObject> securityVisitor =
new BuildingBeanVisitor<>(security, new JsonDataSink(BlotterUtils.getJsonBuildingConverters()));
BeanTraverser securityTraverser = BlotterUtils.securityJsonBuildingTraverser();
JSONObject securityJson = (JSONObject) securityTraverser.traverse(securityMetaBean, securityVisitor);
if (security instanceof FinancialSecurity) {
UnderlyingSecurityVisitor visitor = new UnderlyingSecurityVisitor(VersionCorrection.LATEST, _securityMaster);
ManageableSecurity underlying = ((FinancialSecurity) security).accept(visitor);
if (underlying != null) {
BeanVisitor<JSONObject> underlyingVisitor =
new BuildingBeanVisitor<>(underlying, new JsonDataSink(BlotterUtils.getJsonBuildingConverters()));
MetaBean underlyingMetaBean = s_metaBeansByTypeName.get(underlying.getClass().getSimpleName());
JSONObject underlyingJson = (JSONObject) securityTraverser.traverse(underlyingMetaBean, underlyingVisitor);
root.put("underlying", underlyingJson);
}
}
root.put("security", securityJson);
} else {
_fungibleTradeBuilder.extractTradeData(trade, tradeSink, BlotterUtils.getStringConvert());
}
JSONObject tradeJson = tradeSink.finish();
root.put("trade", tradeJson);
} catch (JSONException e) {
throw new OpenGammaRuntimeException("Failed to build JSON", e);
}
return root.toString();
}
private static boolean isOtc(ManageableSecurity security) {
if (security instanceof FinancialSecurity) {
return ((FinancialSecurity) security).accept(new OtcSecurityVisitor());
} else {
return false;
}
}
/**
* Finds the security referred to by securityLink. This basically does the same thing as resolving the link but
* it returns a {@link ManageableSecurity} instead of a {@link Security}.
* @param securityLink Contains the security ID(s)
* @return The security, not null
* @throws DataNotFoundException If a matching security can't be found
*/
private ManageableSecurity findSecurity(ManageableSecurityLink securityLink) {
SecurityDocument securityDocument;
if (securityLink.getObjectId() != null) {
// TODO do we definitely want the LATEST?
securityDocument = _securityMaster.get(securityLink.getObjectId(), VersionCorrection.LATEST);
} else {
SecuritySearchRequest searchRequest = new SecuritySearchRequest(securityLink.getExternalId());
SecuritySearchResult searchResult = _securityMaster.search(searchRequest);
securityDocument = searchResult.getFirstDocument();
if (securityDocument == null) {
throw new DataNotFoundException("No security found with external IDs " + securityLink.getExternalId());
}
}
return securityDocument.getSecurity();
}
@POST
@Path("trades")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public Response createTrade(@Context UriInfo uriInfo, @FormParam("trade") String jsonStr) {
// TODO need to make sure this can't be used for updating existing trades, only for creating new ones
try {
JSONObject json = new JSONObject(jsonStr);
JSONObject tradeJson = json.getJSONObject("trade");
UniqueId nodeId = UniqueId.parse(json.getString("nodeId"));
String tradeTypeName = tradeJson.getString("type");
// TODO tell don't ask - it is an option to ask each of the trade builders? but their interfaces are different
// they would have to know how to extract the subsections of the JSON
UniqueId tradeId;
if (tradeTypeName.equals(OtcTradeBuilder.TRADE_TYPE_NAME)) {
tradeId = createOtcTrade(json, tradeJson, nodeId);
} else if (tradeTypeName.equals(FungibleTradeBuilder.TRADE_TYPE_NAME)) {
tradeId = _fungibleTradeBuilder.addTrade(new JsonBeanDataSource(tradeJson), nodeId);
} else {
throw new IllegalArgumentException("Unknown trade type " + tradeTypeName);
}
URI createdTradeUri = uriInfo.getAbsolutePathBuilder().path(tradeId.getObjectId().toString()).build();
return Response.status(Response.Status.CREATED).header("Location", createdTradeUri).build();
} catch (JSONException e) {
throw new IllegalArgumentException("Failed to parse JSON", e);
}
}
/* TODO endpoint for updating positions that don't have any trades?
create a dummy trade for the position amount?
what about
* positions with trades whose position and trade amounts are inconsistent
* adding trade to positions with no trades but a position amount? create 2 trades?
* editing a position but not by adding a trade? leave empty and update the amount?
should editing a position always leave it with a consistent set of trades? even if it's empty?
*/
@PUT
@Path("positions/{positionIdStr}")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public void updatePosition(@FormParam("trade") String jsonStr, @PathParam("positionIdStr") String positionIdStr) {
try {
UniqueId positionId = UniqueId.parse(positionIdStr);
JSONObject json = new JSONObject(jsonStr);
JSONObject tradeJson = json.getJSONObject("trade");
String tradeTypeName = tradeJson.getString("type");
// TODO tell don't ask - ask each of the existing trade builders until one of them can handle it?
if (tradeTypeName.equals(OtcTradeBuilder.TRADE_TYPE_NAME)) {
updateOtcPosition(positionId, json, tradeJson);
} else if (tradeTypeName.equals(FungibleTradeBuilder.TRADE_TYPE_NAME)) {
_fungibleTradeBuilder.updatePosition(new JsonBeanDataSource(tradeJson), positionId);
} else {
throw new IllegalArgumentException("Unknown trade type " + tradeTypeName);
}
} catch (JSONException e) {
throw new IllegalArgumentException("Failed to parse JSON", e);
}
}
@PUT
@Path("trades/{tradeIdStr}")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public void updateTrade(@FormParam("trade") String jsonStr, @PathParam("tradeIdStr") String tradeIdStr) {
try {
// TODO what should happen to this? the ID is also in the JSON. check they match?
@SuppressWarnings("unused")
UniqueId tradeId = UniqueId.parse(tradeIdStr);
JSONObject json = new JSONObject(jsonStr);
JSONObject tradeJson = json.getJSONObject("trade");
String tradeTypeName = tradeJson.getString("type");
// TODO tell don't ask - ask each of the existing trade builders until one of them can handle it?
if (tradeTypeName.equals(OtcTradeBuilder.TRADE_TYPE_NAME)) {
createOtcTrade(json, tradeJson, null);
} else if (tradeTypeName.equals(FungibleTradeBuilder.TRADE_TYPE_NAME)) {
_fungibleTradeBuilder.updateTrade(new JsonBeanDataSource(tradeJson));
} else {
throw new IllegalArgumentException("Unknown trade type " + tradeTypeName);
}
} catch (JSONException e) {
throw new IllegalArgumentException("Failed to parse JSON", e);
}
}
// TODO this could move inside the builder
private UniqueId createOtcTrade(JSONObject json, JSONObject tradeJson, UniqueId nodeId) {
try {
JSONObject securityJson = json.getJSONObject("security");
JSONObject underlyingJson = json.optJSONObject("underlying");
BeanDataSource tradeData = new JsonBeanDataSource(tradeJson);
BeanDataSource securityData = new JsonBeanDataSource(securityJson);
BeanDataSource underlyingData;
if (underlyingJson != null) {
underlyingData = new JsonBeanDataSource(underlyingJson);
} else {
underlyingData = null;
}
if (nodeId == null) {
return _otcTradeBuilder.updateTrade(tradeData, securityData, underlyingData);
} else {
return _otcTradeBuilder.addTrade(tradeData, securityData, underlyingData, nodeId);
}
} catch (JSONException e) {
throw new IllegalArgumentException("Failed to parse JSON", e);
}
}
private UniqueId updateOtcPosition(UniqueId positionId, JSONObject json, JSONObject tradeJson) {
try {
JSONObject securityJson = json.getJSONObject("security");
JSONObject underlyingJson = json.optJSONObject("underlying");
BeanDataSource tradeData = new JsonBeanDataSource(tradeJson);
BeanDataSource securityData = new JsonBeanDataSource(securityJson);
BeanDataSource underlyingData;
if (underlyingJson != null) {
underlyingData = new JsonBeanDataSource(underlyingJson);
} else {
underlyingData = null;
}
return _otcTradeBuilder.updatePosition(positionId, tradeData, securityData, underlyingData);
} catch (JSONException e) {
throw new IllegalArgumentException("Failed to parse JSON", e);
}
}
private static Map<Object, Object> map(Object... values) {
final Map<Object, Object> result = Maps.newHashMap();
for (int i = 0; i < values.length / 2; i++) {
result.put(values[i * 2], values[(i * 2) + 1]);
}
result.put("now", ZonedDateTime.now(OpenGammaClock.getInstance()));
return result;
}
@Path("lookup")
public BlotterLookupResource getLookupResource() {
return new BlotterLookupResource(BlotterUtils.getStringConvert());
}
}