/**
* Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.pricer.fxopt;
import static com.opengamma.strata.basics.currency.Currency.EUR;
import static com.opengamma.strata.basics.currency.Currency.GBP;
import static com.opengamma.strata.basics.currency.Currency.USD;
import static com.opengamma.strata.basics.date.DayCounts.ACT_365F;
import static com.opengamma.strata.collect.TestHelper.coverBeanEquals;
import static com.opengamma.strata.collect.TestHelper.coverImmutableBean;
import static com.opengamma.strata.collect.TestHelper.date;
import static com.opengamma.strata.market.curve.interpolator.CurveExtrapolators.FLAT;
import static com.opengamma.strata.market.curve.interpolator.CurveInterpolators.LINEAR;
import static org.testng.Assert.assertEquals;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Iterator;
import org.testng.annotations.Test;
import com.google.common.collect.ImmutableList;
import com.opengamma.strata.basics.currency.CurrencyPair;
import com.opengamma.strata.collect.array.DoubleArray;
import com.opengamma.strata.collect.array.DoubleMatrix;
import com.opengamma.strata.market.param.CurrencyParameterSensitivity;
import com.opengamma.strata.market.param.ParameterMetadata;
/**
* Test {@link BlackFxOptionSmileVolatilities}.
*/
@Test
public class BlackFxOptionSmileVolatilitiesTest {
private static final FxOptionVolatilitiesName NAME = FxOptionVolatilitiesName.of("Test");
private static final DoubleArray TIME_TO_EXPIRY = DoubleArray.of(0.01, 0.252, 0.501, 1.0, 2.0, 5.0);
private static final DoubleArray ATM = DoubleArray.of(0.175, 0.185, 0.18, 0.17, 0.16, 0.16);
private static final DoubleArray DELTA = DoubleArray.of(0.10, 0.25);
private static final DoubleMatrix RISK_REVERSAL = DoubleMatrix.ofUnsafe(new double[][] {
{-0.010, -0.0050 }, {-0.011, -0.0060 }, {-0.012, -0.0070 }, {-0.013, -0.0080 }, {-0.014, -0.0090 }, {-0.014, -0.0090 } });
private static final DoubleMatrix STRANGLE = DoubleMatrix.ofUnsafe(new double[][] {
{0.0300, 0.0100 }, {0.0310, 0.0110 }, {0.0320, 0.0120 }, {0.0330, 0.0130 }, {0.0340, 0.0140 }, {0.0340, 0.0140 } });
private static final InterpolatedStrikeSmileDeltaTermStructure SMILE_TERM = InterpolatedStrikeSmileDeltaTermStructure.of(
TIME_TO_EXPIRY, DELTA, ATM, RISK_REVERSAL, STRANGLE, ACT_365F);
private static final LocalDate VAL_DATE = date(2015, 2, 17);
private static final LocalTime VAL_TIME = LocalTime.of(13, 45);
private static final ZoneId LONDON_ZONE = ZoneId.of("Europe/London");
private static final ZonedDateTime VAL_DATE_TIME = VAL_DATE.atTime(VAL_TIME).atZone(LONDON_ZONE);
private static final CurrencyPair CURRENCY_PAIR = CurrencyPair.of(EUR, USD);
private static final BlackFxOptionSmileVolatilities VOLS =
BlackFxOptionSmileVolatilities.of(NAME, CURRENCY_PAIR, VAL_DATE_TIME, SMILE_TERM);
private static final LocalTime TIME = LocalTime.of(11, 45);
private static final ZonedDateTime[] TEST_EXPIRY = new ZonedDateTime[] {
date(2015, 2, 18).atTime(LocalTime.MIDNIGHT).atZone(LONDON_ZONE),
date(2015, 9, 17).atTime(TIME).atZone(LONDON_ZONE),
date(2016, 6, 17).atTime(TIME).atZone(LONDON_ZONE),
date(2018, 7, 17).atTime(TIME).atZone(LONDON_ZONE) };
private static final double[] FORWARD = new double[] {1.4, 1.395, 1.39, 1.38, 1.35 };
private static final int NB_EXPIRY = TEST_EXPIRY.length;
private static final double[] TEST_STRIKE = new double[] {1.1, 1.28, 1.45, 1.62, 1.8 };
private static final int NB_STRIKE = TEST_STRIKE.length;
private static final double TOLERANCE = 1.0E-12;
private static final double EPS = 1.0E-7;
//-------------------------------------------------------------------------
public void test_builder() {
BlackFxOptionSmileVolatilities test = BlackFxOptionSmileVolatilities.builder()
.name(NAME)
.currencyPair(CURRENCY_PAIR)
.smile(SMILE_TERM)
.valuationDateTime(VAL_DATE_TIME)
.build();
assertEquals(test.getName(), NAME);
assertEquals(test.getValuationDateTime(), VAL_DATE_TIME);
assertEquals(test.getCurrencyPair(), CURRENCY_PAIR);
assertEquals(test.getSmile(), SMILE_TERM);
assertEquals(VOLS, test);
}
//-------------------------------------------------------------------------
public void test_volatility() {
for (int i = 0; i < NB_EXPIRY; i++) {
double expiryTime = VOLS.relativeTime(TEST_EXPIRY[i]);
for (int j = 0; j < NB_STRIKE; ++j) {
double volExpected = SMILE_TERM.volatility(expiryTime, TEST_STRIKE[j], FORWARD[i]);
double volComputed = VOLS.volatility(CURRENCY_PAIR, TEST_EXPIRY[i], TEST_STRIKE[j], FORWARD[i]);
assertEquals(volComputed, volExpected, TOLERANCE);
}
}
}
public void test_volatility_inverse() {
for (int i = 0; i < NB_EXPIRY; i++) {
double expiryTime = VOLS.relativeTime(TEST_EXPIRY[i]);
for (int j = 0; j < NB_STRIKE; ++j) {
double volExpected = SMILE_TERM.volatility(expiryTime, TEST_STRIKE[j], FORWARD[i]);
double volComputed = VOLS.volatility(CURRENCY_PAIR.inverse(), TEST_EXPIRY[i], 1d / TEST_STRIKE[j],
1d / FORWARD[i]);
assertEquals(volComputed, volExpected, TOLERANCE);
}
}
}
//-------------------------------------------------------------------------
public void test_surfaceParameterSensitivity() {
for (int i = 0; i < NB_EXPIRY; i++) {
for (int j = 0; j < NB_STRIKE; ++j) {
double timeToExpiry = VOLS.relativeTime(TEST_EXPIRY[i]);
FxOptionSensitivity sensi = FxOptionSensitivity.of(
VOLS.getName(), CURRENCY_PAIR, timeToExpiry, TEST_STRIKE[j], FORWARD[i], GBP, 1d);
CurrencyParameterSensitivity computed = VOLS.parameterSensitivity(sensi).getSensitivities().get(0);
Iterator<ParameterMetadata> itr = computed.getParameterMetadata().iterator();
for (double value : computed.getSensitivity().toArray()) {
FxVolatilitySurfaceYearFractionParameterMetadata meta = ((FxVolatilitySurfaceYearFractionParameterMetadata) itr.next());
double nodeExpiry = meta.getYearFraction();
double nodeDelta = meta.getStrike().getValue();
double expected = nodeSensitivity(
VOLS, CURRENCY_PAIR, TEST_EXPIRY[i], TEST_STRIKE[j], FORWARD[i], nodeExpiry, nodeDelta);
assertEquals(value, expected, EPS);
}
}
}
}
public void test_surfaceParameterSensitivity_inverse() {
for (int i = 0; i < NB_EXPIRY; i++) {
for (int j = 0; j < NB_STRIKE; ++j) {
double timeToExpiry = VOLS.relativeTime(TEST_EXPIRY[i]);
FxOptionSensitivity sensi = FxOptionSensitivity.of(
VOLS.getName(), CURRENCY_PAIR.inverse(), timeToExpiry, 1d / TEST_STRIKE[j], 1d / FORWARD[i], GBP, 1d);
CurrencyParameterSensitivity computed = VOLS.parameterSensitivity(sensi).getSensitivities().get(0);
Iterator<ParameterMetadata> itr = computed.getParameterMetadata().iterator();
for (double value : computed.getSensitivity().toArray()) {
FxVolatilitySurfaceYearFractionParameterMetadata meta = ((FxVolatilitySurfaceYearFractionParameterMetadata) itr.next());
double nodeExpiry = meta.getYearFraction();
double nodeDelta = meta.getStrike().getValue();
double expected = nodeSensitivity(VOLS, CURRENCY_PAIR.inverse(),
TEST_EXPIRY[i], 1d / TEST_STRIKE[j], 1d / FORWARD[i], nodeExpiry, nodeDelta);
assertEquals(value, expected, EPS);
}
}
}
}
//-------------------------------------------------------------------------
public void coverage() {
BlackFxOptionSmileVolatilities test1 =
BlackFxOptionSmileVolatilities.of(NAME, CURRENCY_PAIR, VAL_DATE_TIME, SMILE_TERM);
coverImmutableBean(test1);
BlackFxOptionSmileVolatilities test2 = BlackFxOptionSmileVolatilities.of(
FxOptionVolatilitiesName.of("Boo"),
CURRENCY_PAIR.inverse(),
ZonedDateTime.of(2015, 12, 21, 11, 15, 0, 0, ZoneId.of("Z")),
SMILE_TERM);
coverBeanEquals(test1, test2);
}
//-------------------------------------------------------------------------
// bumping a node point at (nodeExpiry, nodeDelta)
private double nodeSensitivity(
BlackFxOptionSmileVolatilities provider,
CurrencyPair pair,
ZonedDateTime expiry,
double strike,
double forward,
double nodeExpiry,
double nodeDelta) {
double strikeMod = provider.getCurrencyPair().equals(pair) ? strike : 1.0 / strike;
double forwardMod = provider.getCurrencyPair().equals(pair) ? forward : 1.0 / forward;
InterpolatedStrikeSmileDeltaTermStructure smileTerm =
(InterpolatedStrikeSmileDeltaTermStructure) provider.getSmile();
double[] times = smileTerm.getExpiries().toArray();
int nTimes = times.length;
SmileDeltaParameters[] volTermUp = new SmileDeltaParameters[nTimes];
SmileDeltaParameters[] volTermDw = new SmileDeltaParameters[nTimes];
int deltaIndex = -1;
for (int i = 0; i < nTimes; ++i) {
DoubleArray deltas = smileTerm.getVolatilityTerm().get(i).getDelta();
int nDeltas = deltas.size();
int nDeltasTotal = 2 * nDeltas + 1;
double[] deltasTotal = new double[nDeltasTotal];
for (int j = 0; j < nDeltas; ++j) {
deltasTotal[j] = 1d - deltas.get(j);
deltasTotal[2 * nDeltas - j] = deltas.get(j);
}
double[] volsUp = smileTerm.getVolatilityTerm().get(i).getVolatility().toArray();
double[] volsDw = smileTerm.getVolatilityTerm().get(i).getVolatility().toArray();
if (Math.abs(times[i] - nodeExpiry) < TOLERANCE) {
for (int j = 0; j < nDeltasTotal; ++j) {
if (Math.abs(deltasTotal[j] - nodeDelta) < TOLERANCE) {
deltaIndex = j;
volsUp[j] += EPS;
volsDw[j] -= EPS;
}
}
}
volTermUp[i] = SmileDeltaParameters.of(times[i], deltas, DoubleArray.copyOf(volsUp));
volTermDw[i] = SmileDeltaParameters.of(times[i], deltas, DoubleArray.copyOf(volsDw));
}
InterpolatedStrikeSmileDeltaTermStructure smileTermUp =
InterpolatedStrikeSmileDeltaTermStructure.of(ImmutableList.copyOf(volTermUp), ACT_365F);
InterpolatedStrikeSmileDeltaTermStructure smileTermDw =
InterpolatedStrikeSmileDeltaTermStructure.of(ImmutableList.copyOf(volTermDw), ACT_365F);
BlackFxOptionSmileVolatilities provUp =
BlackFxOptionSmileVolatilities.of(NAME, CURRENCY_PAIR, VAL_DATE_TIME, smileTermUp);
BlackFxOptionSmileVolatilities provDw =
BlackFxOptionSmileVolatilities.of(NAME, CURRENCY_PAIR, VAL_DATE_TIME, smileTermDw);
double volUp = provUp.volatility(pair, expiry, strike, forward);
double volDw = provDw.volatility(pair, expiry, strike, forward);
double totalSensi = 0.5 * (volUp - volDw) / EPS;
double expiryTime = provider.relativeTime(expiry);
SmileDeltaParameters singleSmile = smileTerm.smileForExpiry(expiryTime);
double[] strikesUp = singleSmile.strike(forwardMod).toArray();
double[] strikesDw = strikesUp.clone();
double[] vols = singleSmile.getVolatility().toArray();
strikesUp[deltaIndex] += EPS;
strikesDw[deltaIndex] -= EPS;
double volStrikeUp =
LINEAR.bind(DoubleArray.ofUnsafe(strikesUp), DoubleArray.ofUnsafe(vols), FLAT, FLAT).interpolate(strikeMod);
double volStrikeDw =
LINEAR.bind(DoubleArray.ofUnsafe(strikesDw), DoubleArray.ofUnsafe(vols), FLAT, FLAT).interpolate(strikeMod);
double sensiStrike = 0.5 * (volStrikeUp - volStrikeDw) / EPS;
SmileDeltaParameters singleSmileUp = smileTermUp.smileForExpiry(expiryTime);
double strikeUp = singleSmileUp.strike(forwardMod).get(deltaIndex);
SmileDeltaParameters singleSmileDw = smileTermDw.smileForExpiry(expiryTime);
double strikeDw = singleSmileDw.strike(forwardMod).get(deltaIndex);
double sensiVol = 0.5 * (strikeUp - strikeDw) / EPS;
return totalSensi - sensiStrike * sensiVol;
}
}