/**
* Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.basics.currency;
import java.io.Serializable;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.joda.beans.Bean;
import org.joda.beans.BeanBuilder;
import org.joda.beans.BeanDefinition;
import org.joda.beans.ImmutableBean;
import org.joda.beans.ImmutableValidator;
import org.joda.beans.JodaBeanUtils;
import org.joda.beans.MetaProperty;
import org.joda.beans.Property;
import org.joda.beans.PropertyDefinition;
import org.joda.beans.impl.direct.DirectFieldsBeanBuilder;
import org.joda.beans.impl.direct.DirectMetaBean;
import org.joda.beans.impl.direct.DirectMetaProperty;
import org.joda.beans.impl.direct.DirectMetaPropertyMap;
import com.google.common.math.DoubleMath;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.collect.Messages;
/**
* A single foreign exchange rate between two currencies, such as 'EUR/USD 1.25'.
* <p>
* This represents a rate of foreign exchange. The rate 'EUR/USD 1.25' consists of three
* elements - the base currency 'EUR', the counter currency 'USD' and the rate '1.25'.
* When performing a conversion a rate of '1.25' means that '1 EUR = 1.25 USD'.
* <p>
* See {@link CurrencyPair} for the representation that does not contain a rate.
* <p>
* This class is immutable and thread-safe.
*/
@BeanDefinition(builderScope = "private")
public final class FxRate
implements FxRateProvider, ImmutableBean, Serializable {
/**
* Regular expression to parse the textual format.
*/
private static final Pattern REGEX_FORMAT = Pattern.compile("([A-Z]{3})[/]([A-Z]{3})[ ]([0-9+.-]+)");
/**
* The currency pair.
* The pair is formed of two parts, the base and the counter.
* In the pair 'AAA/BBB' the base is 'AAA' and the counter is 'BBB'.
*/
@PropertyDefinition(validate = "notNull")
private final CurrencyPair pair;
/**
* The rate applicable to the currency pair.
* One unit of the base currency is exchanged for this amount of the counter currency.
*/
@PropertyDefinition(validate = "ArgChecker.notNegativeOrZero", get = "private")
private final double rate;
//-------------------------------------------------------------------------
/**
* Obtains an instance from two currencies.
* <p>
* The first currency is the base and the second is the counter.
* The two currencies may be the same, but if they are then the rate must be one.
*
* @param base the base currency
* @param counter the counter currency
* @param rate the conversion rate, greater than zero
* @return the FX rate
* @throws IllegalArgumentException if the rate is invalid
*/
public static FxRate of(Currency base, Currency counter, double rate) {
return new FxRate(CurrencyPair.of(base, counter), rate);
}
/**
* Obtains an instance from a currency pair.
* <p>
* The two currencies may be the same, but if they are then the rate must be one.
*
* @param pair the currency pair
* @param rate the conversion rate, greater than zero
* @return the FX rate
* @throws IllegalArgumentException if the rate is invalid
*/
public static FxRate of(CurrencyPair pair, double rate) {
return new FxRate(pair, rate);
}
//-------------------------------------------------------------------------
/**
* Parses a rate from a string with format AAA/BBB RATE.
* <p>
* The parsed format is '${baseCurrency}/${counterCurrency} ${rate}'.
* Currency parsing is case insensitive.
*
* @param rateStr the rate as a string AAA/BBB RATE
* @return the FX rate
* @throws IllegalArgumentException if the FX rate cannot be parsed
*/
public static FxRate parse(String rateStr) {
ArgChecker.notNull(rateStr, "rateStr");
Matcher matcher = REGEX_FORMAT.matcher(rateStr.toUpperCase(Locale.ENGLISH));
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid rate: " + rateStr);
}
try {
Currency base = Currency.parse(matcher.group(1));
Currency counter = Currency.parse(matcher.group(2));
double rate = Double.parseDouble(matcher.group(3));
return new FxRate(CurrencyPair.of(base, counter), rate);
} catch (RuntimeException ex) {
throw new IllegalArgumentException("Unable to parse rate: " + rateStr, ex);
}
}
//-------------------------------------------------------------------------
@ImmutableValidator
private void validate() {
if (pair.getBase().equals(pair.getCounter()) && rate != 1d) {
throw new IllegalArgumentException("Conversion rate between identical currencies must be one");
}
}
//-------------------------------------------------------------------------
/**
* Gets the inverse rate.
* <p>
* The inverse rate has the same currencies but in reverse order.
* The rate is the reciprocal of the original.
*
* @return the inverse pair
*/
public FxRate inverse() {
return new FxRate(pair.inverse(), 1d / rate);
}
/**
* Gets the FX rate for the specified currency pair.
* <p>
* The rate returned is the rate from the base currency to the counter currency
* as defined by this formula: {@code (1 * baseCurrency = fxRate * counterCurrency)}.
* <p>
* This will return the rate or inverse rate, or 1 if the two input currencies are the same.
*
* @param baseCurrency the base currency, to convert from
* @param counterCurrency the counter currency, to convert to
* @return the FX rate for the currency pair
* @throws IllegalArgumentException if no FX rate could be found
*/
@Override
public double fxRate(Currency baseCurrency, Currency counterCurrency) {
if (baseCurrency.equals(counterCurrency)) {
return 1d;
}
if (baseCurrency.equals(pair.getBase()) && counterCurrency.equals(pair.getCounter())) {
return rate;
}
if (counterCurrency.equals(pair.getBase()) && baseCurrency.equals(pair.getCounter())) {
return 1d / rate;
}
throw new IllegalArgumentException(Messages.format(
"No FX rate found for {}/{}", baseCurrency, counterCurrency));
}
/**
* Derives an FX rate from two related FX rates.
* <p>
* Given two FX rates it is possible to derive another rate if they have a currency in common.
* For example, given rates for EUR/GBP and EUR/CHF it is possible to derive rates for GBP/CHF.
* The result will always have a currency pair in the conventional order.
* <p>
* The cross is only returned if the two pairs contains three currencies in total.
* If the inputs are invalid, an exception is thrown.
* <ul>
* <li>AAA/BBB and BBB/CCC - valid, producing AAA/CCC
* <li>AAA/BBB and CCC/BBB - valid, producing AAA/CCC
* <li>AAA/BBB and BBB/AAA - invalid, exception thrown
* <li>AAA/BBB and BBB/BBB - invalid, exception thrown
* <li>AAA/BBB and CCC/DDD - invalid, exception thrown
* </ul>
*
* @param other the other rates
* @return a set of FX rates derived from these rates and the other rates
* @throws IllegalArgumentException if the cross rate cannot be calculated
*/
public FxRate crossRate(FxRate other) {
return pair.cross(other.pair).map(cross -> computeCross(this, other, cross))
.orElseThrow(() -> new IllegalArgumentException(Messages.format(
"Unable to cross when no unique common currency: {} and {}", pair, other.pair)));
}
// computes the cross rate
private static FxRate computeCross(FxRate fx1, FxRate fx2, CurrencyPair crossPairAC) {
// aim is to convert AAA/BBB and BBB/CCC to AAA/CCC
Currency currA = crossPairAC.getBase();
Currency currC = crossPairAC.getCounter();
// given the conventional cross rate pair, order the two rates to match
boolean crossBaseCurrencyInFx1 = fx1.pair.contains(currA);
FxRate fxABorBA = crossBaseCurrencyInFx1 ? fx1 : fx2;
FxRate fxBCorCB = crossBaseCurrencyInFx1 ? fx2 : fx1;
// extract the rates, taking the inverse if the pair is in the inverse order
double rateAB = fxABorBA.getPair().getBase().equals(currA) ? fxABorBA.rate : 1d / fxABorBA.rate;
double rateBC = fxBCorCB.getPair().getCounter().equals(currC) ? fxBCorCB.rate : 1d / fxBCorCB.rate;
return FxRate.of(crossPairAC, rateAB * rateBC);
}
/**
* Returns an FX rate object representing the market convention rate between the two currencies.
* <p>
* If the currency pair is the market convention pair, this method returns {@code this}, otherwise
* it returns an {@code FxRate} with the inverse currency pair and reciprocal rate.
*
* @return an FX rate object representing the market convention rate between the two currencies
*/
public FxRate toConventional() {
return pair.isConventional() ? this : FxRate.of(pair.toConventional(), 1 / rate);
}
//-------------------------------------------------------------------------
/**
* Returns the formatted string version of the currency pair.
* <p>
* The format is '${baseCurrency}/${counterCurrency} ${rate}'.
*
* @return the formatted string
*/
@Override
public String toString() {
return pair + " " + (DoubleMath.isMathematicalInteger(rate) ? Long.toString((long) rate) : Double.toString(rate));
}
//------------------------- AUTOGENERATED START -------------------------
///CLOVER:OFF
/**
* The meta-bean for {@code FxRate}.
* @return the meta-bean, not null
*/
public static FxRate.Meta meta() {
return FxRate.Meta.INSTANCE;
}
static {
JodaBeanUtils.registerMetaBean(FxRate.Meta.INSTANCE);
}
/**
* The serialization version id.
*/
private static final long serialVersionUID = 1L;
private FxRate(
CurrencyPair pair,
double rate) {
JodaBeanUtils.notNull(pair, "pair");
ArgChecker.notNegativeOrZero(rate, "rate");
this.pair = pair;
this.rate = rate;
validate();
}
@Override
public FxRate.Meta metaBean() {
return FxRate.Meta.INSTANCE;
}
@Override
public <R> Property<R> property(String propertyName) {
return metaBean().<R>metaProperty(propertyName).createProperty(this);
}
@Override
public Set<String> propertyNames() {
return metaBean().metaPropertyMap().keySet();
}
//-----------------------------------------------------------------------
/**
* Gets the currency pair.
* The pair is formed of two parts, the base and the counter.
* In the pair 'AAA/BBB' the base is 'AAA' and the counter is 'BBB'.
* @return the value of the property, not null
*/
public CurrencyPair getPair() {
return pair;
}
//-----------------------------------------------------------------------
/**
* Gets the rate applicable to the currency pair.
* One unit of the base currency is exchanged for this amount of the counter currency.
* @return the value of the property
*/
private double getRate() {
return rate;
}
//-----------------------------------------------------------------------
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj != null && obj.getClass() == this.getClass()) {
FxRate other = (FxRate) obj;
return JodaBeanUtils.equal(pair, other.pair) &&
JodaBeanUtils.equal(rate, other.rate);
}
return false;
}
@Override
public int hashCode() {
int hash = getClass().hashCode();
hash = hash * 31 + JodaBeanUtils.hashCode(pair);
hash = hash * 31 + JodaBeanUtils.hashCode(rate);
return hash;
}
//-----------------------------------------------------------------------
/**
* The meta-bean for {@code FxRate}.
*/
public static final class Meta extends DirectMetaBean {
/**
* The singleton instance of the meta-bean.
*/
static final Meta INSTANCE = new Meta();
/**
* The meta-property for the {@code pair} property.
*/
private final MetaProperty<CurrencyPair> pair = DirectMetaProperty.ofImmutable(
this, "pair", FxRate.class, CurrencyPair.class);
/**
* The meta-property for the {@code rate} property.
*/
private final MetaProperty<Double> rate = DirectMetaProperty.ofImmutable(
this, "rate", FxRate.class, Double.TYPE);
/**
* The meta-properties.
*/
private final Map<String, MetaProperty<?>> metaPropertyMap$ = new DirectMetaPropertyMap(
this, null,
"pair",
"rate");
/**
* Restricted constructor.
*/
private Meta() {
}
@Override
protected MetaProperty<?> metaPropertyGet(String propertyName) {
switch (propertyName.hashCode()) {
case 3433178: // pair
return pair;
case 3493088: // rate
return rate;
}
return super.metaPropertyGet(propertyName);
}
@Override
public BeanBuilder<? extends FxRate> builder() {
return new FxRate.Builder();
}
@Override
public Class<? extends FxRate> beanType() {
return FxRate.class;
}
@Override
public Map<String, MetaProperty<?>> metaPropertyMap() {
return metaPropertyMap$;
}
//-----------------------------------------------------------------------
/**
* The meta-property for the {@code pair} property.
* @return the meta-property, not null
*/
public MetaProperty<CurrencyPair> pair() {
return pair;
}
/**
* The meta-property for the {@code rate} property.
* @return the meta-property, not null
*/
public MetaProperty<Double> rate() {
return rate;
}
//-----------------------------------------------------------------------
@Override
protected Object propertyGet(Bean bean, String propertyName, boolean quiet) {
switch (propertyName.hashCode()) {
case 3433178: // pair
return ((FxRate) bean).getPair();
case 3493088: // rate
return ((FxRate) bean).getRate();
}
return super.propertyGet(bean, propertyName, quiet);
}
@Override
protected void propertySet(Bean bean, String propertyName, Object newValue, boolean quiet) {
metaProperty(propertyName);
if (quiet) {
return;
}
throw new UnsupportedOperationException("Property cannot be written: " + propertyName);
}
}
//-----------------------------------------------------------------------
/**
* The bean-builder for {@code FxRate}.
*/
private static final class Builder extends DirectFieldsBeanBuilder<FxRate> {
private CurrencyPair pair;
private double rate;
/**
* Restricted constructor.
*/
private Builder() {
}
//-----------------------------------------------------------------------
@Override
public Object get(String propertyName) {
switch (propertyName.hashCode()) {
case 3433178: // pair
return pair;
case 3493088: // rate
return rate;
default:
throw new NoSuchElementException("Unknown property: " + propertyName);
}
}
@Override
public Builder set(String propertyName, Object newValue) {
switch (propertyName.hashCode()) {
case 3433178: // pair
this.pair = (CurrencyPair) newValue;
break;
case 3493088: // rate
this.rate = (Double) newValue;
break;
default:
throw new NoSuchElementException("Unknown property: " + propertyName);
}
return this;
}
@Override
public Builder set(MetaProperty<?> property, Object value) {
super.set(property, value);
return this;
}
@Override
public Builder setString(String propertyName, String value) {
setString(meta().metaProperty(propertyName), value);
return this;
}
@Override
public Builder setString(MetaProperty<?> property, String value) {
super.setString(property, value);
return this;
}
@Override
public Builder setAll(Map<String, ? extends Object> propertyValueMap) {
super.setAll(propertyValueMap);
return this;
}
@Override
public FxRate build() {
return new FxRate(
pair,
rate);
}
//-----------------------------------------------------------------------
@Override
public String toString() {
StringBuilder buf = new StringBuilder(96);
buf.append("FxRate.Builder{");
buf.append("pair").append('=').append(JodaBeanUtils.toString(pair)).append(',').append(' ');
buf.append("rate").append('=').append(JodaBeanUtils.toString(rate));
buf.append('}');
return buf.toString();
}
}
///CLOVER:ON
//-------------------------- AUTOGENERATED END --------------------------
}