com.opengamma.strata.basics.currency.FxRate.java Source code

Java tutorial

Introduction

Here is the source code for com.opengamma.strata.basics.currency.FxRate.java

Source

/**
 * 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 --------------------------
}