/**
* 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.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.joda.convert.FromString;
import org.joda.convert.ToString;
import com.google.common.collect.ImmutableMap;
import com.opengamma.strata.collect.ArgChecker;
/**
* An ordered pair of currencies, such as 'EUR/USD'.
* <p>
* This could be used to identify a pair of currencies for quoting rates in FX deals.
* See {@link FxRate} for the representation that contains a rate.
* <p>
* It is recommended to define currencies in advance using the {@code CurrencyPair.ini} file.
* Standard configuration includes many commonly used currency pairs.
* <p>
* Only currencies listed in configuration will be returned by {@link #getAvailablePairs()}.
* If a pair is requested that is not defined in configuration, it will still be created,
* however the market convention information will be generated.
* <p>
* This class is immutable and thread-safe.
*/
public final class CurrencyPair
implements Serializable {
/** Serialization version. */
private static final long serialVersionUID = 1L;
/**
* Regular expression to parse the textual format.
* Three ASCII upper case letters, a slash, and another three ASCII upper case letters.
*/
static final Pattern REGEX_FORMAT = Pattern.compile("([A-Z]{3})/([A-Z]{3})");
/**
* The configured instances and associated rate digits.
*/
private static final ImmutableMap<CurrencyPair, Integer> CONFIGURED = CurrencyDataLoader.loadPairs();
/**
* Ordering of each currency, used when choosing a market convention pair when there is no configuration.
* The currency closer to the start of the list (with the lower ordering) is the base currency.
*/
private static final ImmutableMap<Currency, Integer> CURRENCY_ORDERING = CurrencyDataLoader.loadOrdering();
/**
* The base currency of the pair.
* In the pair 'AAA/BBB' the base is 'AAA'.
*/
private final Currency base;
/**
* The counter currency of the pair.
* In the pair 'AAA/BBB' the counter is 'BBB'.
*/
private final Currency counter;
//-------------------------------------------------------------------------
/**
* Obtains the set of configured currency pairs.
* <p>
* This contains all the currency pairs that have been defined in configuration.
* Any currency pair instances that have been dynamically created are not included.
*
* @return an immutable set containing all registered currency pairs
*/
public static Set<CurrencyPair> getAvailablePairs() {
return CONFIGURED.keySet();
}
//-------------------------------------------------------------------------
/**
* 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.
*
* @param base the base currency
* @param counter the counter currency
* @return the currency pair
*/
public static CurrencyPair of(Currency base, Currency counter) {
ArgChecker.notNull(base, "base");
ArgChecker.notNull(counter, "counter");
return new CurrencyPair(base, counter);
}
/**
* Parses a currency pair from a string with format AAA/BBB.
* <p>
* The parsed format is '${baseCurrency}/${counterCurrency}'.
* Currency parsing is case insensitive.
*
* @param pairStr the currency pair as a string AAA/BBB
* @return the currency pair
* @throws IllegalArgumentException if the pair cannot be parsed
*/
@FromString
public static CurrencyPair parse(String pairStr) {
ArgChecker.notNull(pairStr, "pairStr");
Matcher matcher = REGEX_FORMAT.matcher(pairStr.toUpperCase(Locale.ENGLISH));
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid currency pair: " + pairStr);
}
Currency base = Currency.parse(matcher.group(1));
Currency counter = Currency.parse(matcher.group(2));
return new CurrencyPair(base, counter);
}
//-------------------------------------------------------------------------
/**
* Creates a currency pair.
*
* @param base the base currency, validated not null
* @param counter the counter currency, validated not null
*/
private CurrencyPair(Currency base, Currency counter) {
this.base = base;
this.counter = counter;
}
//-------------------------------------------------------------------------
/**
* Gets the inverse currency pair.
* <p>
* The inverse pair has the same currencies but in reverse order.
*
* @return the inverse pair
*/
public CurrencyPair inverse() {
return new CurrencyPair(counter, base);
}
/**
* Checks if the currency pair contains the supplied currency as either its base or counter.
*
* @param currency the currency to check against the pair
* @return true if the currency is either the base or counter currency in the pair
*/
public boolean contains(Currency currency) {
ArgChecker.notNull(currency, "currency");
return base.equals(currency) || counter.equals(currency);
}
/**
* Checks if this currency pair is an identity pair.
* <p>
* The identity pair is one where the base and counter currency are the same..
*
* @return true if this pair is an identity pair
*/
public boolean isIdentity() {
return base.equals(counter);
}
/**
* Checks if this currency pair is the inverse of the specified pair.
* <p>
* This could be used to check if an FX rate specified in one currency pair needs inverting.
*
* @param other the other currency pair
* @return true if the currency is the inverse of the specified pair
*/
public boolean isInverse(CurrencyPair other) {
ArgChecker.notNull(other, "currencyPair");
return base.equals(other.counter) && counter.equals(other.base);
}
/**
* Finds the currency pair that is a cross between this pair and the other pair.
* <p>
* The cross is only returned if the two pairs contains three currencies in total,
* such as AAA/BBB and BBB/CCC and neither pair is an identity such as AAA/AAA.
* <ul>
* <li>Given two pairs AAA/BBB and BBB/CCC the result will be AAA/CCC or CCC/AAA as per the market convention.
* <li>Given two pairs AAA/BBB and CCC/DDD the result will be empty.
* <li>Given two pairs AAA/AAA and AAA/BBB the result will be empty.
* <li>Given two pairs AAA/BBB and AAA/BBB the result will be empty.
* <li>Given two pairs AAA/AAA and AAA/AAA the result will be empty.
* </ul>
*
* @param other the other currency pair
* @return the cross currency pair, or empty if no cross currency pair can be created
*/
public Optional<CurrencyPair> cross(CurrencyPair other) {
ArgChecker.notNull(other, "other");
if (isIdentity() || other.isIdentity() || this.equals(other) || this.equals(other.inverse())) {
return Optional.empty();
}
// AAA/BBB cross BBB/CCC
if (counter.equals(other.base)) {
return Optional.of(of(base, other.counter).toConventional());
}
// AAA/BBB cross CCC/BBB
if (counter.equals(other.counter)) {
return Optional.of(of(base, other.base).toConventional());
}
// BBB/AAA cross BBB/CCC
if (base.equals(other.base)) {
return Optional.of(of(counter, other.counter).toConventional());
}
// BBB/AAA cross CCC/BBB
if (base.equals(other.counter)) {
return Optional.of(of(counter, other.base).toConventional());
}
return Optional.empty();
}
//-------------------------------------------------------------------------
/**
* Checks if this currency pair is a conventional currency pair.
* <p>
* A market convention determines that 'EUR/USD' should be used and not 'USD/EUR'.
* This knowledge is encoded in configuration for a standard set of pairs.
* <p>
* It is possible to create two different currency pairs from any two currencies, and it is guaranteed that
* exactly one of the pairs will be the market convention pair.
* <p>
* If a currency pair is not explicitly listed in the configuration, a priority ordering of currencies
* is used to choose base currency of the pair that is treated as conventional.
* <p>
* If there is no configuration available to determine which pair is the market convention, a pair will
* be chosen arbitrarily but deterministically. This ensures the same pair will be chosen for any two
* currencies even if the {@code CurrencyPair} instances are created independently.
*
* @return true if the currency pair follows the market convention, false if it does not
*/
public boolean isConventional() {
// If the pair is in the configuration file it is a market convention pair
if (CONFIGURED.containsKey(this)) {
return true;
}
// Get the priorities of the currencies to determine which should be the base
Integer basePriority = CURRENCY_ORDERING.getOrDefault(base, Integer.MAX_VALUE);
Integer counterPriority = CURRENCY_ORDERING.getOrDefault(counter, Integer.MAX_VALUE);
// If a currency is earlier in the list it has a higher priority
if (basePriority < counterPriority) {
return true;
} else if (basePriority > counterPriority) {
return false;
}
// Neither currency is included in the list defining the ordering.
// Use lexicographical ordering. It's arbitrary but consistent. This ensures two CurrencyPair instances
// created independently for the same two currencies will always choose the same conventional pair.
// The natural ordering of Currency is the same as the natural ordering of the currency code but
// comparing the Currency instances is more efficient.
// This is <= 0 so that a pair with two copies of the same currency is conventional
return base.compareTo(counter) <= 0;
}
/**
* Returns the market convention currency pair for the currencies in the pair.
* <p>
* If {@link #isConventional()} is {@code true} this method returns {@code this}, otherwise
* it returns the {@link #inverse} pair.
*
* @return the market convention currency pair for the currencies in the pair
*/
public CurrencyPair toConventional() {
return isConventional() ? this : inverse();
}
/**
* Gets the number of digits in the rate.
* <p>
* If this rate is a conventional currency pair defined in configuration,
* then the number of digits in a market FX rate quote is returned.
* <p>
* If the currency pair is not defined in configuration the sum of the
* {@link Currency#getMinorUnitDigits() minor unit digits} from the two currencies is returned.
*
* @return the number of digits in the FX rate
*/
public int getRateDigits() {
Integer digits = CONFIGURED.get(this);
if (digits != null) {
return digits;
}
Integer inverseDigits = CONFIGURED.get(inverse());
if (inverseDigits != null) {
return inverseDigits;
}
return base.getMinorUnitDigits() + counter.getMinorUnitDigits();
}
//-------------------------------------------------------------------------
/**
* Gets the base currency of the pair.
* <p>
* In the pair 'AAA/BBB' the base is 'AAA'.
*
* @return the base currency
*/
public Currency getBase() {
return base;
}
//-------------------------------------------------------------------------
/**
* Gets the counter currency of the pair.
* <p>
* In the pair 'AAA/BBB' the counter is 'BBB'.
* <p>
* The counter currency is also known as the <i>quote currency</i> or the <i>variable currency</i>.
*
* @return the counter currency
*/
public Currency getCounter() {
return counter;
}
//-------------------------------------------------------------------------
/**
* Checks if this currency pair equals another.
* <p>
* The comparison checks the two currencies.
*
* @param obj the other currency pair, null returns false
* @return true if equal
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj != null && obj.getClass() == this.getClass()) {
CurrencyPair other = (CurrencyPair) obj;
return base.equals(other.base) &&
counter.equals(other.counter);
}
return false;
}
/**
* Returns a suitable hash code for the currency.
*
* @return the hash code
*/
@Override
public int hashCode() {
return base.hashCode() ^ counter.hashCode();
}
//-------------------------------------------------------------------------
/**
* Returns the formatted string version of the currency pair.
* <p>
* The format is '${baseCurrency}/${counterCurrency}'.
*
* @return the formatted string
*/
@Override
@ToString
public String toString() {
return base.getCode() + "/" + counter.getCode();
}
}