/**
* Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.financial.currency;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.threeten.bp.LocalDate;
import org.threeten.bp.Period;
import com.google.common.annotations.VisibleForTesting;
import com.opengamma.engine.ComputationTarget;
import com.opengamma.engine.ComputationTargetSpecification;
import com.opengamma.engine.function.AbstractFunction;
import com.opengamma.engine.function.FunctionCompilationContext;
import com.opengamma.engine.function.FunctionExecutionContext;
import com.opengamma.engine.function.FunctionInputs;
import com.opengamma.engine.target.ComputationTargetType;
import com.opengamma.engine.value.ComputedValue;
import com.opengamma.engine.value.ValueProperties;
import com.opengamma.engine.value.ValuePropertyNames;
import com.opengamma.engine.value.ValueRequirement;
import com.opengamma.engine.value.ValueRequirementNames;
import com.opengamma.engine.value.ValueSpecification;
import com.opengamma.financial.analytics.LabelledObjectMatrix1D;
import com.opengamma.financial.analytics.TenorLabelledLocalDateDoubleTimeSeriesMatrix1D;
import com.opengamma.timeseries.DoubleTimeSeries;
import com.opengamma.timeseries.date.localdate.LocalDateDoubleTimeSeries;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.time.Tenor;
/**
* Converts a series of values from one currency to another, preserving all other properties.
*/
public class CurrencySeriesConversionFunction extends AbstractFunction.NonCompiledInvoker {
private static final String CURRENCY_INJECTION_PROPERTY = ValuePropertyNames.OUTPUT_RESERVED_PREFIX + "Currency";
private static final Logger s_logger = LoggerFactory.getLogger(CurrencySeriesConversionFunction.class);
private static final ComputationTargetType TYPE = ComputationTargetType.PORTFOLIO_NODE.or(ComputationTargetType.POSITION).or(ComputationTargetType.SECURITY).or(ComputationTargetType.TRADE);
private final Set<String> _valueNames;
private boolean _allowViewDefaultCurrency; // = false;
public CurrencySeriesConversionFunction(final String valueName) {
ArgumentChecker.notNull(valueName, "valueName");
_valueNames = Collections.singleton(valueName);
}
public CurrencySeriesConversionFunction(final String... valueNames) {
ArgumentChecker.notEmpty(valueNames, "valueNames");
_valueNames = new HashSet<String>(Arrays.asList(valueNames));
}
protected Set<String> getValueNames() {
return _valueNames;
}
public void setAllowViewDefaultCurrency(final boolean allowViewDefaultCurrency) {
_allowViewDefaultCurrency = allowViewDefaultCurrency;
}
public boolean isAllowViewDefaultCurrency() {
return _allowViewDefaultCurrency;
}
private ValueRequirement getInputValueRequirement(final ComputationTargetSpecification targetSpec, final ValueRequirement desiredValue) {
return new ValueRequirement(desiredValue.getValueName(), targetSpec, desiredValue.getConstraints().copy().withoutAny(CURRENCY_INJECTION_PROPERTY)
.withAny(ValuePropertyNames.CURRENCY).get());
}
private ValueRequirement getInputValueRequirement(final ComputationTargetSpecification targetSpec, final ValueRequirement desiredValue, final String forceCurrency) {
return new ValueRequirement(desiredValue.getValueName(), targetSpec, desiredValue.getConstraints().copy().withoutAny(ValuePropertyNames.CURRENCY).with(
ValuePropertyNames.CURRENCY, forceCurrency).withOptional(CURRENCY_INJECTION_PROPERTY).get());
}
protected DoubleTimeSeries<LocalDate> convertDouble(final double value, final DoubleTimeSeries<LocalDate> conversionRates) {
return conversionRates.divide(value).reciprocal();
}
protected Double convertDouble(final double value, final double conversionRate) {
return value / conversionRate;
}
protected DoubleTimeSeries<LocalDate> convertTimeSeries(final DoubleTimeSeries<LocalDate> values, final DoubleTimeSeries<LocalDate> conversionRates) {
return values.divide(conversionRates);
}
@VisibleForTesting
/* package */ TenorLabelledLocalDateDoubleTimeSeriesMatrix1D convertLabelledMatrix(final LabelledObjectMatrix1D<Tenor, LocalDateDoubleTimeSeries, Period> values, final DoubleTimeSeries<LocalDate> conversionRates) {
LocalDateDoubleTimeSeries[] convertedValues = new LocalDateDoubleTimeSeries[values.size()];
for (int i = 0; i < values.size(); i++) {
convertedValues[i] = (LocalDateDoubleTimeSeries) convertTimeSeries(values.getValues()[i], conversionRates);
}
return new TenorLabelledLocalDateDoubleTimeSeriesMatrix1D(values.getKeys(), values.getLabels(), values.getLabelsTitle(), convertedValues, values.getValuesTitle());
}
protected DoubleTimeSeries<LocalDate> convertTimeSeries(final DoubleTimeSeries<LocalDate> values, final double conversionRate) {
return values.divide(conversionRate);
}
@SuppressWarnings("unchecked")
protected Object convertValue(final ComputedValue inputValue, final ValueRequirement desiredValue, final DoubleTimeSeries<LocalDate> conversionRates) {
final Object value = inputValue.getValue();
if (value instanceof Double) {
return convertDouble((Double) value, conversionRates);
} else if (value instanceof DoubleTimeSeries) {
// TODO: Note the unchecked cast. We'll either get a zero intersection and empty result if the rates aren't the same type or a class cast exception.
return convertTimeSeries((DoubleTimeSeries) value, conversionRates);
} else if (value instanceof TenorLabelledLocalDateDoubleTimeSeriesMatrix1D) {
// Try to make this more generic
return convertLabelledMatrix((TenorLabelledLocalDateDoubleTimeSeriesMatrix1D) value, conversionRates);
} else {
s_logger.error("Can't convert object with type {} to {}", inputValue.getValue().getClass(), desiredValue);
return null;
}
}
@SuppressWarnings("unchecked")
protected Object convertValue(final ComputedValue inputValue, final ValueRequirement desiredValue, final Double conversionRate) {
final Object value = inputValue.getValue();
if (value instanceof Double) {
return convertDouble((Double) value, conversionRate);
} else if (value instanceof DoubleTimeSeries) {
return convertTimeSeries((DoubleTimeSeries) value, conversionRate);
} else {
s_logger.error("Can't convert object with type {} to {}", inputValue.getValue().getClass(), desiredValue);
return null;
}
}
@SuppressWarnings("unchecked")
@Override
public Set<ComputedValue> execute(final FunctionExecutionContext executionContext, final FunctionInputs inputs, final ComputationTarget target, final Set<ValueRequirement> desiredValues) {
ComputedValue inputValue = null;
DoubleTimeSeries<LocalDate> exchangeRates = null;
Double exchangeRate = null;
for (final ComputedValue input : inputs.getAllValues()) {
if (ValueRequirementNames.HISTORICAL_FX_TIME_SERIES.equals(input.getSpecification().getValueName())) {
if (input.getValue() instanceof Double) {
// Note: The rate might be a DOUBLE if the matrix being used has a hard coded constant in it. Improbable for a time-series conversion, but possible.
exchangeRate = (Double) input.getValue();
} else if (input.getValue() instanceof DoubleTimeSeries) {
// TODO: Note the unchecked cast. We'll either get a zero intersection and empty result if the values aren't the same type or a class cast exception.
exchangeRates = (DoubleTimeSeries) input.getValue();
} else {
return null;
}
} else {
inputValue = input;
}
}
if (inputValue == null) {
return null;
}
final ValueRequirement desiredValue = desiredValues.iterator().next();
final String outputCurrency = desiredValue.getConstraint(ValuePropertyNames.CURRENCY);
final String inputCurrency = inputValue.getSpecification().getProperty(ValuePropertyNames.CURRENCY);
if (outputCurrency.equals(inputCurrency)) {
// Don't think this should happen
return Collections.singleton(inputValue);
} else {
s_logger.debug("Converting from {} to {}", inputCurrency, outputCurrency);
final Object converted;
if (exchangeRates != null) {
converted = convertValue(inputValue, desiredValue, exchangeRates);
} else if (exchangeRate != null) {
converted = convertValue(inputValue, desiredValue, exchangeRate);
} else {
return null;
}
if (converted != null) {
return Collections.singleton(new ComputedValue(new ValueSpecification(desiredValue.getValueName(), target.toSpecification(), desiredValue.getConstraints()), converted));
} else {
return null;
}
}
}
@Override
public Set<ValueRequirement> getRequirements(final FunctionCompilationContext context, final ComputationTarget target, final ValueRequirement desiredValue) {
final Set<String> possibleCurrencies = desiredValue.getConstraints().getValues(ValuePropertyNames.CURRENCY);
if (possibleCurrencies == null) {
s_logger.debug("Must specify a currency constraint; use DefaultCurrencyFunction instead");
return null;
} else if (possibleCurrencies.isEmpty()) {
if (isAllowViewDefaultCurrency()) {
// The original function may not have delivered a result because it had heterogeneous input currencies, so try forcing the view default
final String defaultCurrencyISO = DefaultCurrencyFunction.getViewDefaultCurrencyISO(context);
if (defaultCurrencyISO == null) {
s_logger.debug("No default currency from the view to inject");
return null;
}
s_logger.debug("Injecting view default currency {}", defaultCurrencyISO);
return Collections.singleton(getInputValueRequirement(target.toSpecification(), desiredValue, defaultCurrencyISO));
} else {
s_logger.debug("Cannot satisfy a wildcard currency constraint");
return null;
}
} else {
// Actual input requirement is desired requirement with the currency wild-carded
return Collections.singleton(getInputValueRequirement(target.toSpecification(), desiredValue));
}
}
@Override
public Set<ValueSpecification> getResults(final FunctionCompilationContext context, final ComputationTarget target) {
// Maximal set of outputs is the valueNames with the infinite property set
final ComputationTargetSpecification targetSpec = target.toSpecification();
if (getValueNames().size() == 1) {
return Collections.singleton(new ValueSpecification(getValueNames().iterator().next(), targetSpec, ValueProperties.all()));
}
final Set<ValueSpecification> result = new HashSet<ValueSpecification>();
for (final String valueName : getValueNames()) {
result.add(new ValueSpecification(valueName, targetSpec, ValueProperties.all()));
}
return result;
}
@Override
public Set<ValueSpecification> getResults(final FunctionCompilationContext context, final ComputationTarget target, final Map<ValueSpecification, ValueRequirement> inputs) {
final Map.Entry<ValueSpecification, ValueRequirement> input = inputs.entrySet().iterator().next();
if (input.getValue().getConstraints().getValues(CURRENCY_INJECTION_PROPERTY) == null) {
// Resolved output is the input with the currency wild-carded, and the function ID the same (this is so that after composition the node might
// be removed from the graph)
final ValueSpecification value = input.getKey();
return Collections.singleton(new ValueSpecification(value.getValueName(), value.getTargetSpecification(), value.getProperties().copy().withAny(ValuePropertyNames.CURRENCY).get()));
}
// The input was requested with the converted currency, so return the same specification to remove this node from the graph
return Collections.singleton(input.getKey());
}
private String getCurrency(final Collection<ValueSpecification> specifications) {
final ValueSpecification specification = specifications.iterator().next();
final Set<String> currencies = specification.getProperties().getValues(ValuePropertyNames.CURRENCY);
if ((currencies == null) || (currencies.size() != 1)) {
return null;
}
return currencies.iterator().next();
}
@Override
public Set<ValueRequirement> getAdditionalRequirements(final FunctionCompilationContext context, final ComputationTarget target, final Set<ValueSpecification> inputs,
final Set<ValueSpecification> outputs) {
s_logger.debug("FX requirements for {} -> {}", inputs, outputs);
final String inputCurrency = getCurrency(inputs);
if (inputCurrency == null) {
return null;
}
final String outputCurrency = getCurrency(outputs);
if (outputCurrency == null) {
return null;
}
if (inputCurrency.equals(outputCurrency)) {
return Collections.emptySet();
}
return Collections.singleton(CurrencyMatrixSeriesSourcingFunction.getConversionRequirement(inputCurrency, outputCurrency));
}
@Override
public ComputationTargetType getTargetType() {
return TYPE;
}
}