// -------------------------------------------------------------------------
 @Override
 public double volatility(
     double forward, double strike, double timeToExpiry, SabrFormulaData data) {
   ArgChecker.notNull(data, "data");
   double alpha = data.getAlpha();
   double beta = data.getBeta();
   double rho = data.getRho();
   double nu = data.getNu();
   return volatility(forward, strike, timeToExpiry, alpha, beta, rho, nu);
 }
 @Test
 /**
  * Test if the Hagan volatility function implementation around ATM is numerically stable enough
  * (the finite difference slope should be small enough).
  */
 public void testATMSmoothness() {
   double timeToExpiry = 1;
   double alpha = 0.05;
   double beta = 0.5;
   double nu = 0.50;
   double rho = -0.25;
   int nbPoints = 100;
   double forward = 0.05;
   double[] sabrVolatilty = new double[2 * nbPoints + 1];
   double range = 5E-9;
   double strike[] = new double[2 * nbPoints + 1];
   for (int looppts = -nbPoints; looppts <= nbPoints; looppts++) {
     strike[looppts + nbPoints] = forward + ((double) looppts) / nbPoints * range;
     SabrFormulaData SabrData = SabrFormulaData.of(alpha, beta, rho, nu);
     sabrVolatilty[looppts + nbPoints] =
         FUNCTION.getVolatility(forward, strike[looppts + nbPoints], timeToExpiry, SabrData);
   }
   for (int looppts = -nbPoints; looppts < nbPoints; looppts++) {
     assertTrue(
         Math.abs(sabrVolatilty[looppts + nbPoints + 1] - sabrVolatilty[looppts + nbPoints])
                 / (strike[looppts + nbPoints + 1] - strike[looppts + nbPoints])
             < 20.0);
   }
 }
 @Test
 public void testVolatilityAdjointLargeRhoZGreaterThan1() {
   double eps = 1e-11;
   double tol = 1e-4;
   SabrFormulaData data = DATA.withRho(1.0 - 1e-9).withAlpha(0.15 * ALPHA);
   testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
 }
 @Test
 public void testVolatilityAdjointLargeRhoZLessThan1() {
   double eps = 1e-4;
   double tol = 1e-5;
   SabrFormulaData data = DATA.withRho(1.0 - 1e-9);
   testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
 }
 /** Test the rho = 1 edge case */
 @Test
 public void testVolatilityAdjointRho1() {
   double eps = 1e-4;
   double tol = 1e-5;
   SabrFormulaData data = DATA.withRho(1.0);
   testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
   testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
   testVolatilityAdjoint(F, CALL_OTM, data, eps, tol);
 }
 /** Test the nu = 0 edge case */
 @Test
 public void testVolatilityAdjointNu0() {
   double eps = 1e-5;
   double tol = 1e-6;
   SabrFormulaData data = DATA.withNu(0.0);
   testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
   testVolatilityAdjoint(F, CALL_ITM, data, eps, 2e-4);
   testVolatilityAdjoint(F, CALL_OTM, data, eps, 5e-5);
 }
 /** Test the beta = 1 edge case */
 @Test
 public void testVolatilityAdjointBeta1() {
   double eps = 1e-6;
   double tol = 1e-6;
   SabrFormulaData data = DATA.withBeta(1.0);
   testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
   testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
   testVolatilityAdjoint(F, CALL_OTM, data, eps, tol);
 }
 @Test
 public void testVolatilityAdjointSmallAlpha() {
   double eps = 1e-7;
   double tol = 1e-3;
   SabrFormulaData data = DATA.withAlpha(1e-5);
   testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
   testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
   testVolatilityAdjoint(F, CALL_OTM, data, eps, tol);
 }
  /** Calculate the true SABR delta and gamma and compare with that found by finite difference */
  @Test(enabled = false)
  public void testGreeks() {
    double eps = 1e-3;
    double f = 1.2;
    double k = 1.4;
    double t = 5.0;
    double alpha = 0.3;
    double beta = 0.6;
    double rho = -0.4;
    double nu = 0.4;
    SabrFormulaData sabrData = SabrFormulaData.of(alpha, beta, rho, nu);
    ValueDerivatives adj = FUNCTION.getVolatilityAdjoint(f, k, t, sabrData);
    double bsDelta = BlackFormulaRepository.delta(f, k, t, adj.getValue(), true);
    double bsVega = BlackFormulaRepository.vega(f, k, t, adj.getValue());
    double volForwardSense = adj.getDerivative(1);
    double delta = bsDelta + bsVega * volForwardSense;

    SabrFormulaData data = SabrFormulaData.of(alpha, beta, rho, nu);
    double volUp = FUNCTION.getVolatility(f + eps, k, t, data);
    double volDown = FUNCTION.getVolatility(f - eps, k, t, data);
    double priceUp = BlackFormulaRepository.price(f + eps, k, t, volUp, true);
    double price = BlackFormulaRepository.price(f, k, t, adj.getValue(), true);
    double priceDown = BlackFormulaRepository.price(f - eps, k, t, volDown, true);
    double fdDelta = (priceUp - priceDown) / 2 / eps;
    assertEquals(fdDelta, delta, 1e-6);

    double bsVanna = BlackFormulaRepository.vanna(f, k, t, adj.getValue());
    double bsGamma = BlackFormulaRepository.gamma(f, k, t, adj.getValue());

    double[] volD1 = new double[5];
    double[][] volD2 = new double[2][2];
    FUNCTION.getVolatilityAdjoint2(f, k, t, sabrData, volD1, volD2);
    double d2Sigmad2Fwd = volD2[0][0];
    double gamma = bsGamma + 2 * bsVanna * adj.getDerivative(1) + bsVega * d2Sigmad2Fwd;
    double fdGamma = (priceUp + priceDown - 2 * price) / eps / eps;

    double d2Sigmad2FwdFD = (volUp + volDown - 2 * adj.getValue()) / eps / eps;
    assertEquals(d2Sigmad2FwdFD, d2Sigmad2Fwd, 1e-4);

    assertEquals(fdGamma, gamma, 1e-2);
  }
  // TODO write a fuzzer that hits SABR with random parameters
  @Test(enabled = false)
  public void testRandomParameters() {
    double eps = 1e-5;
    double tol = 1e-3;

    for (int count = 0; count < 100; count++) {
      double alpha = Math.exp(NORMAL.nextRandom() * 0.2 - 2);
      double beta = Math.random(); // TODO Uniform numbers in distribution
      double nu = Math.exp(NORMAL.nextRandom() * 0.3 - 1);
      double rho = 2 * Math.random() - 1;
      SabrFormulaData data = SabrFormulaData.of(alpha, beta, rho, nu);
      testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
      testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
      testVolatilityAdjoint(F, CALL_OTM, data, eps, tol);
    }
  }
 /**
  * Test the alpha = 0 edge case. Implied vol is zero for alpha = 0, and except in the ATM case,
  * the alpha sensitivity is infinite. We choose to (arbitrarily) return 1e7 in this case.
  */
 @Test
 public void testVolatilityAdjointAlpha0() {
   double eps = 1e-5;
   double tol = 1e-6;
   SabrFormulaData data = DATA.withAlpha(0.0);
   testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
   double volatility = FUNCTION.getVolatility(F, STRIKE_ITM, T, data);
   ValueDerivatives volatilityAdjoint = FUNCTION.getVolatilityAdjoint(F, STRIKE_ITM, T, data);
   assertEquals(volatility, volatilityAdjoint.getValue(), tol);
   assertEquals(0.0, volatilityAdjoint.getDerivative(0), tol);
   assertEquals(0.0, volatilityAdjoint.getDerivative(1), tol);
   assertEquals(1e7, volatilityAdjoint.getDerivative(2), tol);
   assertEquals(0.0, volatilityAdjoint.getDerivative(3), tol);
   assertEquals(0.0, volatilityAdjoint.getDerivative(4), tol);
   assertEquals(0.0, volatilityAdjoint.getDerivative(5), tol);
 }
 private void testVolatilityAdjoint(
     double forward,
     EuropeanVanillaOption optionData,
     SabrFormulaData sabrData,
     double eps,
     double tol) {
   double volatility =
       FUNCTION.getVolatility(
           forward, optionData.getStrike(), optionData.getTimeToExpiry(), sabrData);
   double[] volatilityAdjoint =
       toArray(
           FUNCTION.getVolatilityAdjoint(
               forward, optionData.getStrike(), optionData.getTimeToExpiry(), sabrData));
   assertEquals(volatility, volatilityAdjoint[0], tol);
   assertEqualsRelTol(
       "Forward Sensitivity" + sabrData.toString(),
       fdSensitivity(optionData, forward, sabrData, SabrParameter.Forward, eps),
       volatilityAdjoint[1],
       tol);
   assertEqualsRelTol(
       "Strike Sensitivity" + sabrData.toString(),
       fdSensitivity(optionData, forward, sabrData, SabrParameter.Strike, eps),
       volatilityAdjoint[2],
       tol);
   assertEqualsRelTol(
       "Alpha Sensitivity" + sabrData.toString(),
       fdSensitivity(optionData, forward, sabrData, SabrParameter.Alpha, eps),
       volatilityAdjoint[3],
       tol);
   assertEqualsRelTol(
       "Beta Sensitivity" + sabrData.toString(),
       fdSensitivity(optionData, forward, sabrData, SabrParameter.Beta, eps),
       volatilityAdjoint[4],
       tol);
   assertEqualsRelTol(
       "Rho Sensitivity" + sabrData.toString(),
       fdSensitivity(optionData, forward, sabrData, SabrParameter.Rho, eps),
       volatilityAdjoint[5],
       tol);
   assertEqualsRelTol(
       "Nu Sensitivity" + sabrData.toString(),
       fdSensitivity(optionData, forward, sabrData, SabrParameter.Nu, eps),
       volatilityAdjoint[6],
       tol);
 }
  @SuppressWarnings("null")
  private double fdSensitivity(
      EuropeanVanillaOption optionData,
      double forward,
      SabrFormulaData sabrData,
      SabrParameter param,
      double delta) {

    Function<SabrFormulaData, Double> funcC = null;
    Function<SabrFormulaData, Double> funcB = null;
    Function<SabrFormulaData, Double> funcA = null;
    SabrFormulaData dataC = null;
    SabrFormulaData dataB = sabrData;
    SabrFormulaData dataA = null;
    Function<SabrFormulaData, Double> func = getVolatilityFunction(optionData, forward);

    FiniteDifferenceType fdType = null;

    switch (param) {
      case Strike:
        double strike = optionData.getStrike();
        if (strike >= delta) {
          fdType = FiniteDifferenceType.CENTRAL;
          funcA = getVolatilityFunction(withStrike(optionData, strike - delta), forward);
          funcC = getVolatilityFunction(withStrike(optionData, strike + delta), forward);
        } else {
          fdType = FiniteDifferenceType.FORWARD;
          funcA = func;
          funcB = getVolatilityFunction(withStrike(optionData, strike + delta), forward);
          funcC = getVolatilityFunction(withStrike(optionData, strike + 2 * delta), forward);
        }
        dataC = sabrData;
        dataB = sabrData;
        dataA = sabrData;
        break;
      case Forward:
        if (forward > delta) {
          fdType = FiniteDifferenceType.CENTRAL;
          funcA = getVolatilityFunction(optionData, forward - delta);
          funcC = getVolatilityFunction(optionData, forward + delta);
        } else {
          fdType = FiniteDifferenceType.FORWARD;
          funcA = func;
          funcB = getVolatilityFunction(optionData, forward + delta);
          funcC = getVolatilityFunction(optionData, forward + 2 * delta);
        }
        dataC = sabrData;
        dataB = sabrData;
        dataA = sabrData;
        break;
      case Alpha:
        double a = sabrData.getAlpha();
        if (a >= delta) {
          fdType = FiniteDifferenceType.CENTRAL;
          dataA = sabrData.withAlpha(a - delta);
          dataC = sabrData.withAlpha(a + delta);
        } else {
          fdType = FiniteDifferenceType.FORWARD;
          dataA = sabrData;
          dataB = sabrData.withAlpha(a + delta);
          dataC = sabrData.withAlpha(a + 2 * delta);
        }
        funcC = func;
        funcB = func;
        funcA = func;
        break;
      case Beta:
        double b = sabrData.getBeta();
        if (b >= delta) {
          fdType = FiniteDifferenceType.CENTRAL;
          dataA = sabrData.withBeta(b - delta);
          dataC = sabrData.withBeta(b + delta);
        } else {
          fdType = FiniteDifferenceType.FORWARD;
          dataA = sabrData;
          dataB = sabrData.withBeta(b + delta);
          dataC = sabrData.withBeta(b + 2 * delta);
        }
        funcC = func;
        funcB = func;
        funcA = func;
        break;
      case Nu:
        double n = sabrData.getNu();
        if (n >= delta) {
          fdType = FiniteDifferenceType.CENTRAL;
          dataA = sabrData.withNu(n - delta);
          dataC = sabrData.withNu(n + delta);
        } else {
          fdType = FiniteDifferenceType.FORWARD;
          dataA = sabrData;
          dataB = sabrData.withNu(n + delta);
          dataC = sabrData.withNu(n + 2 * delta);
        }
        funcC = func;
        funcB = func;
        funcA = func;
        break;
      case Rho:
        double r = sabrData.getRho();
        if ((r + 1) < delta) {
          fdType = FiniteDifferenceType.FORWARD;
          dataA = sabrData;
          dataB = sabrData.withRho(r + delta);
          dataC = sabrData.withRho(r + 2 * delta);
        } else if ((1 - r) < delta) {
          fdType = FiniteDifferenceType.BACKWARD;
          dataA = sabrData.withRho(r - 2 * delta);
          dataB = sabrData.withRho(r - delta);
          dataC = sabrData;
        } else {
          fdType = FiniteDifferenceType.CENTRAL;
          dataC = sabrData.withRho(r + delta);
          dataA = sabrData.withRho(r - delta);
        }
        funcC = func;
        funcB = func;
        funcA = func;
        break;
      default:
        throw new MathException("enum not found");
    }

    if (fdType != null) {
      switch (fdType) {
        case FORWARD:
          return (-1.5 * funcA.apply(dataA) + 2.0 * funcB.apply(dataB) - 0.5 * funcC.apply(dataC))
              / delta;
        case BACKWARD:
          return (0.5 * funcA.apply(dataA) - 2.0 * funcB.apply(dataB) + 1.5 * funcC.apply(dataC))
              / delta;
        case CENTRAL:
          return (funcC.apply(dataC) - funcA.apply(dataA)) / 2.0 / delta;
        default:
          throw new MathException("enum not found");
      }
    }
    throw new MathException("enum not found");
  }
/** Test {@link SabrHaganVolatilityFunctionProvider}. */
@Test
public class SabrHaganVolatilityFunctionProviderTest
    extends SabrVolatilityFunctionProviderTestCase {

  private static ProbabilityDistribution<Double> NORMAL =
      new NormalDistribution(0d, 1d, new MersenneTwister64(MersenneTwister.DEFAULT_SEED));
  private static SabrHaganVolatilityFunctionProvider FUNCTION =
      SabrHaganVolatilityFunctionProvider.DEFAULT;

  private static final double ALPHA = 0.05;
  private static final double BETA = 0.50;
  private static final double RHO = -0.25;
  private static final double NU = 0.4;
  private static final double F = 0.05;
  private static final SabrFormulaData DATA = SabrFormulaData.of(ALPHA, BETA, RHO, NU);
  private static final double T = 4.5;
  private static final double STRIKE_ITM = 0.0450;
  private static final double STRIKE_OTM = 0.0550;

  private static EuropeanVanillaOption CALL_ATM = EuropeanVanillaOption.of(F, T, CALL);
  private static EuropeanVanillaOption CALL_ITM = EuropeanVanillaOption.of(STRIKE_ITM, T, CALL);
  private static EuropeanVanillaOption CALL_OTM = EuropeanVanillaOption.of(STRIKE_OTM, T, CALL);

  @Override
  protected VolatilityFunctionProvider<SabrFormulaData> getFunction() {
    return FUNCTION;
  }

  @Test
  /**
   * Test if the Hagan volatility function implementation around ATM is numerically stable enough
   * (the finite difference slope should be small enough).
   */
  public void testATMSmoothness() {
    double timeToExpiry = 1;
    double alpha = 0.05;
    double beta = 0.5;
    double nu = 0.50;
    double rho = -0.25;
    int nbPoints = 100;
    double forward = 0.05;
    double[] sabrVolatilty = new double[2 * nbPoints + 1];
    double range = 5E-9;
    double strike[] = new double[2 * nbPoints + 1];
    for (int looppts = -nbPoints; looppts <= nbPoints; looppts++) {
      strike[looppts + nbPoints] = forward + ((double) looppts) / nbPoints * range;
      SabrFormulaData SabrData = SabrFormulaData.of(alpha, beta, rho, nu);
      sabrVolatilty[looppts + nbPoints] =
          FUNCTION.getVolatility(forward, strike[looppts + nbPoints], timeToExpiry, SabrData);
    }
    for (int looppts = -nbPoints; looppts < nbPoints; looppts++) {
      assertTrue(
          Math.abs(sabrVolatilty[looppts + nbPoints + 1] - sabrVolatilty[looppts + nbPoints])
                  / (strike[looppts + nbPoints + 1] - strike[looppts + nbPoints])
              < 20.0);
    }
  }

  @Test
  /**
   * Tests the first order adjoint derivatives for the SABR Hagan volatility function. The
   * derivatives with respect to the forward, strike, alpha, beta, rho and nu are provided.
   */
  public void testVolatilityAdjointDebug() {
    double eps = 1e-6;
    double tol = 1e-5;
    testVolatilityAdjoint(F, CALL_ATM, DATA, eps, tol);
    testVolatilityAdjoint(F, CALL_ITM, DATA, eps, tol);
    testVolatilityAdjoint(F, CALL_OTM, DATA, eps, tol);
  }

  /**
   * Test small strike edge case. Vol -> infinity as strike -> 0, so the strike is floored - tested
   * against finite difference below this floor will give spurious results
   */
  @Test
  public void testVolatilityAdjointSmallStrike() {
    double eps = 1e-10;
    double tol = 1e-6;
    double strike = 2e-6 * F;
    testVolatilityAdjoint(F, withStrike(CALL_ATM, strike), DATA, eps, tol);
  }

  /**
   * Test the alpha = 0 edge case. Implied vol is zero for alpha = 0, and except in the ATM case,
   * the alpha sensitivity is infinite. We choose to (arbitrarily) return 1e7 in this case.
   */
  @Test
  public void testVolatilityAdjointAlpha0() {
    double eps = 1e-5;
    double tol = 1e-6;
    SabrFormulaData data = DATA.withAlpha(0.0);
    testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
    double volatility = FUNCTION.getVolatility(F, STRIKE_ITM, T, data);
    ValueDerivatives volatilityAdjoint = FUNCTION.getVolatilityAdjoint(F, STRIKE_ITM, T, data);
    assertEquals(volatility, volatilityAdjoint.getValue(), tol);
    assertEquals(0.0, volatilityAdjoint.getDerivative(0), tol);
    assertEquals(0.0, volatilityAdjoint.getDerivative(1), tol);
    assertEquals(1e7, volatilityAdjoint.getDerivative(2), tol);
    assertEquals(0.0, volatilityAdjoint.getDerivative(3), tol);
    assertEquals(0.0, volatilityAdjoint.getDerivative(4), tol);
    assertEquals(0.0, volatilityAdjoint.getDerivative(5), tol);
  }

  @Test
  public void testVolatilityAdjointSmallAlpha() {
    double eps = 1e-7;
    double tol = 1e-3;
    SabrFormulaData data = DATA.withAlpha(1e-5);
    testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
    testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
    testVolatilityAdjoint(F, CALL_OTM, data, eps, tol);
  }

  /** Test the beta = 0 edge case */
  @Test
  public void testVolatilityAdjointBeta0() {
    double eps = 1e-5;
    double tol = 1e-6;
    SabrFormulaData data = DATA.withBeta(0.0);
    testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
    testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
    testVolatilityAdjoint(F, CALL_OTM, data, eps, tol);
  }

  /** Test the beta = 1 edge case */
  @Test
  public void testVolatilityAdjointBeta1() {
    double eps = 1e-6;
    double tol = 1e-6;
    SabrFormulaData data = DATA.withBeta(1.0);
    testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
    testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
    testVolatilityAdjoint(F, CALL_OTM, data, eps, tol);
  }

  /** Test the nu = 0 edge case */
  @Test
  public void testVolatilityAdjointNu0() {
    double eps = 1e-5;
    double tol = 1e-6;
    SabrFormulaData data = DATA.withNu(0.0);
    testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
    testVolatilityAdjoint(F, CALL_ITM, data, eps, 2e-4);
    testVolatilityAdjoint(F, CALL_OTM, data, eps, 5e-5);
  }

  /** Test the rho = -1 edge case */
  @Test
  public void testVolatilityAdjointRhoM1() {
    double eps = 1e-5;
    double tol = 1e-6;
    SabrFormulaData data = DATA.withRho(-1.0);
    testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
    testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
    testVolatilityAdjoint(F, CALL_OTM, data, eps, tol);
  }

  /** Test the rho = 1 edge case */
  @Test
  public void testVolatilityAdjointRho1() {
    double eps = 1e-4;
    double tol = 1e-5;
    SabrFormulaData data = DATA.withRho(1.0);
    testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
    testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
    testVolatilityAdjoint(F, CALL_OTM, data, eps, tol);
  }

  @Test
  public void testVolatilityAdjointLargeRhoZLessThan1() {
    double eps = 1e-4;
    double tol = 1e-5;
    SabrFormulaData data = DATA.withRho(1.0 - 1e-9);
    testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
  }

  @Test
  public void testVolatilityAdjointLargeRhoZGreaterThan1() {
    double eps = 1e-11;
    double tol = 1e-4;
    SabrFormulaData data = DATA.withRho(1.0 - 1e-9).withAlpha(0.15 * ALPHA);
    testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
  }

  @Test
  /**
   * Tests the second order adjoint derivatives for the SABR Hagan volatility function. Only the
   * derivatives with respect to the forward and the strike are provided.
   */
  public void volatilityAdjoint2() {
    volatilityAdjoint2ForInstrument(CALL_ITM, 1.0E-6, 1.0E-2);
    volatilityAdjoint2ForInstrument(
        CALL_ATM, 1.0E-6, 1.0E+2); // ATM the second order derivative is poor.
    volatilityAdjoint2ForInstrument(CALL_OTM, 1.0E-6, 1.0E-2);
  }

  // TODO write a fuzzer that hits SABR with random parameters
  @Test(enabled = false)
  public void testRandomParameters() {
    double eps = 1e-5;
    double tol = 1e-3;

    for (int count = 0; count < 100; count++) {
      double alpha = Math.exp(NORMAL.nextRandom() * 0.2 - 2);
      double beta = Math.random(); // TODO Uniform numbers in distribution
      double nu = Math.exp(NORMAL.nextRandom() * 0.3 - 1);
      double rho = 2 * Math.random() - 1;
      SabrFormulaData data = SabrFormulaData.of(alpha, beta, rho, nu);
      testVolatilityAdjoint(F, CALL_ATM, data, eps, tol);
      testVolatilityAdjoint(F, CALL_ITM, data, eps, tol);
      testVolatilityAdjoint(F, CALL_OTM, data, eps, tol);
    }
  }

  /** Calculate the true SABR delta and gamma and compare with that found by finite difference */
  @Test(enabled = false)
  public void testGreeks() {
    double eps = 1e-3;
    double f = 1.2;
    double k = 1.4;
    double t = 5.0;
    double alpha = 0.3;
    double beta = 0.6;
    double rho = -0.4;
    double nu = 0.4;
    SabrFormulaData sabrData = SabrFormulaData.of(alpha, beta, rho, nu);
    ValueDerivatives adj = FUNCTION.getVolatilityAdjoint(f, k, t, sabrData);
    double bsDelta = BlackFormulaRepository.delta(f, k, t, adj.getValue(), true);
    double bsVega = BlackFormulaRepository.vega(f, k, t, adj.getValue());
    double volForwardSense = adj.getDerivative(1);
    double delta = bsDelta + bsVega * volForwardSense;

    SabrFormulaData data = SabrFormulaData.of(alpha, beta, rho, nu);
    double volUp = FUNCTION.getVolatility(f + eps, k, t, data);
    double volDown = FUNCTION.getVolatility(f - eps, k, t, data);
    double priceUp = BlackFormulaRepository.price(f + eps, k, t, volUp, true);
    double price = BlackFormulaRepository.price(f, k, t, adj.getValue(), true);
    double priceDown = BlackFormulaRepository.price(f - eps, k, t, volDown, true);
    double fdDelta = (priceUp - priceDown) / 2 / eps;
    assertEquals(fdDelta, delta, 1e-6);

    double bsVanna = BlackFormulaRepository.vanna(f, k, t, adj.getValue());
    double bsGamma = BlackFormulaRepository.gamma(f, k, t, adj.getValue());

    double[] volD1 = new double[5];
    double[][] volD2 = new double[2][2];
    FUNCTION.getVolatilityAdjoint2(f, k, t, sabrData, volD1, volD2);
    double d2Sigmad2Fwd = volD2[0][0];
    double gamma = bsGamma + 2 * bsVanna * adj.getDerivative(1) + bsVega * d2Sigmad2Fwd;
    double fdGamma = (priceUp + priceDown - 2 * price) / eps / eps;

    double d2Sigmad2FwdFD = (volUp + volDown - 2 * adj.getValue()) / eps / eps;
    assertEquals(d2Sigmad2FwdFD, d2Sigmad2Fwd, 1e-4);

    assertEquals(fdGamma, gamma, 1e-2);
  }

  /**
   * Check that $\rho \simeq 1$ case is smoothly connected with a general case, i.e., comparing the
   * approximated computation and full computation around the cutoff, which is currently $\rho = 1.0
   * - 1.0e-5$ Note that the resulting numbers contain a large error if $\rho \simeq 1$ and $z
   * \simeq 1$ are true at the same time
   */
  @Test
  public void largeRhoSmoothnessTest() {
    double rhoEps = 1.e-5;
    // rhoIn is larger than the cutoff,
    // thus vol and sensitivities are computed by approximation formulas which are regular in the
    // limit rho -> 1.
    double rhoIn = 1.0 - 0.5 * rhoEps;
    // rhoOut is smaller than the cutoff, thus vol and sensitivities are computed by full formula.
    double rhoOut = 1.0 - 1.5 * rhoEps;
    SabrFormulaData dataIn = SabrFormulaData.of(ALPHA, BETA, rhoIn, NU);
    SabrFormulaData dataOut = SabrFormulaData.of(ALPHA, BETA, rhoOut, NU);

    /*
     * z<1 case, i.e., finite values in the rho->1 limit
     */
    double volatilityOut = FUNCTION.getVolatility(F, STRIKE_OTM, T, dataOut);
    double[] adjointOut = toArray(FUNCTION.getVolatilityAdjoint(F, STRIKE_OTM, T, dataOut));
    double[] volatilityDOut = new double[6];
    double[][] volatilityD2Out = new double[2][2];
    double volatility2Out =
        FUNCTION.getVolatilityAdjoint2(F, STRIKE_OTM, T, dataOut, volatilityDOut, volatilityD2Out);

    double volatilityIn = FUNCTION.getVolatility(F, STRIKE_OTM, T, dataIn);
    double[] adjointIn = toArray(FUNCTION.getVolatilityAdjoint(F, STRIKE_OTM, T, dataIn));
    double[] volatilityDIn = new double[6];
    double[][] volatilityD2In = new double[2][2];
    double volatility2In =
        FUNCTION.getVolatilityAdjoint2(F, STRIKE_OTM, T, dataIn, volatilityDIn, volatilityD2In);

    assertEquals(volatilityOut, volatilityIn, rhoEps);
    assertEquals(volatility2Out, volatility2In, rhoEps);
    for (int i = 0; i < adjointOut.length; ++i) {
      double ref = adjointOut[i];
      assertEquals(adjointOut[i], adjointIn[i], Math.max(Math.abs(ref), 1.0) * 1.e-3);
    }
    for (int i = 0; i < volatilityDOut.length; ++i) {
      double ref = volatilityDOut[i];
      assertEquals(volatilityDOut[i], volatilityDIn[i], Math.max(Math.abs(ref), 1.0) * 1.e-3);
    }

    /*
     * z>1 case, runs into infinity or 0.
     * Convergence speed is much faster (and typically smoother).
     */
    rhoIn = 1.0 - 0.999 * rhoEps;
    rhoOut = 1.0 - 1.001 * rhoEps;
    dataIn = SabrFormulaData.of(ALPHA, BETA, rhoIn, NU);
    dataOut = SabrFormulaData.of(ALPHA, BETA, rhoOut, NU);

    volatilityOut = FUNCTION.getVolatility(3.0 * F, STRIKE_ITM, T, dataOut);
    adjointOut = toArray(FUNCTION.getVolatilityAdjoint(3.0 * F, STRIKE_ITM, T, dataOut));
    volatilityDOut = new double[6];
    volatilityD2Out = new double[2][2];
    volatility2Out =
        FUNCTION.getVolatilityAdjoint2(
            3.0 * F, STRIKE_ITM, T, dataOut, volatilityDOut, volatilityD2Out);

    volatilityIn = FUNCTION.getVolatility(3.0 * F, STRIKE_ITM, T, dataIn);
    adjointIn = toArray(FUNCTION.getVolatilityAdjoint(3.0 * F, STRIKE_ITM, T, dataIn));
    volatilityDIn = new double[6];
    volatilityD2In = new double[2][2];
    volatility2In =
        FUNCTION.getVolatilityAdjoint2(
            3.0 * F, STRIKE_ITM, T, dataIn, volatilityDIn, volatilityD2In);

    assertEquals(volatilityOut, volatilityIn, rhoEps);
    assertEquals(volatility2Out, volatility2In, rhoEps);
    for (int i = 0; i < adjointOut.length; ++i) {
      double ref = adjointOut[i];
      assertEquals(ref, adjointIn[i], Math.max(Math.abs(ref), 1.0) * 1.e-2);
    }
    for (int i = 0; i < volatilityDOut.length; ++i) {
      double ref = volatilityDOut[i];
      assertEquals(ref, volatilityDIn[i], Math.max(Math.abs(ref), 1.0) * 1.e-2);
    }
  }

  public void coverage() {
    coverImmutableBean(FUNCTION);
  }

  public void test_serialization() {
    assertSerialization(FUNCTION);
  }

  // -------------------------------------------------------------------------
  private enum SabrParameter {
    Forward,
    Strike,
    Alpha,
    Beta,
    Nu,
    Rho
  }

  private void testVolatilityAdjoint(
      double forward,
      EuropeanVanillaOption optionData,
      SabrFormulaData sabrData,
      double eps,
      double tol) {
    double volatility =
        FUNCTION.getVolatility(
            forward, optionData.getStrike(), optionData.getTimeToExpiry(), sabrData);
    double[] volatilityAdjoint =
        toArray(
            FUNCTION.getVolatilityAdjoint(
                forward, optionData.getStrike(), optionData.getTimeToExpiry(), sabrData));
    assertEquals(volatility, volatilityAdjoint[0], tol);
    assertEqualsRelTol(
        "Forward Sensitivity" + sabrData.toString(),
        fdSensitivity(optionData, forward, sabrData, SabrParameter.Forward, eps),
        volatilityAdjoint[1],
        tol);
    assertEqualsRelTol(
        "Strike Sensitivity" + sabrData.toString(),
        fdSensitivity(optionData, forward, sabrData, SabrParameter.Strike, eps),
        volatilityAdjoint[2],
        tol);
    assertEqualsRelTol(
        "Alpha Sensitivity" + sabrData.toString(),
        fdSensitivity(optionData, forward, sabrData, SabrParameter.Alpha, eps),
        volatilityAdjoint[3],
        tol);
    assertEqualsRelTol(
        "Beta Sensitivity" + sabrData.toString(),
        fdSensitivity(optionData, forward, sabrData, SabrParameter.Beta, eps),
        volatilityAdjoint[4],
        tol);
    assertEqualsRelTol(
        "Rho Sensitivity" + sabrData.toString(),
        fdSensitivity(optionData, forward, sabrData, SabrParameter.Rho, eps),
        volatilityAdjoint[5],
        tol);
    assertEqualsRelTol(
        "Nu Sensitivity" + sabrData.toString(),
        fdSensitivity(optionData, forward, sabrData, SabrParameter.Nu, eps),
        volatilityAdjoint[6],
        tol);
  }

  private double[] toArray(ValueDerivatives valueDerivatives) {
    double[] derivatives = valueDerivatives.getDerivatives().toArray();
    double[] res = new double[derivatives.length + 1];
    res[0] = valueDerivatives.getValue();
    System.arraycopy(derivatives, 0, res, 1, derivatives.length);
    return res;
  }

  private void volatilityAdjoint2ForInstrument(
      EuropeanVanillaOption option, double tolerance1, double tolerance2) {
    // vol
    double volatility =
        FUNCTION.getVolatility(F, option.getStrike(), option.getTimeToExpiry(), DATA);
    double[] volatilityAdjoint =
        toArray(
            FUNCTION.getVolatilityAdjoint(F, option.getStrike(), option.getTimeToExpiry(), DATA));
    double[] volD = new double[6];
    double[][] volD2 = new double[2][2];
    double vol =
        FUNCTION.getVolatilityAdjoint2(
            F, option.getStrike(), option.getTimeToExpiry(), DATA, volD, volD2);
    assertEquals(volatility, vol, tolerance1);
    // Derivative
    for (int loopder = 0; loopder < 6; loopder++) {
      assertEquals(volatilityAdjoint[loopder + 1], volD[loopder], tolerance1);
    }
    // Derivative forward-forward
    double deltaF = 0.000001;
    double volatilityFP =
        FUNCTION.getVolatility(F + deltaF, option.getStrike(), option.getTimeToExpiry(), DATA);
    double volatilityFM =
        FUNCTION.getVolatility(F - deltaF, option.getStrike(), option.getTimeToExpiry(), DATA);
    double derivativeFF_FD = (volatilityFP + volatilityFM - 2 * volatility) / (deltaF * deltaF);
    assertEquals(derivativeFF_FD, volD2[0][0], tolerance2);
    // Derivative strike-strike
    double deltaK = 0.000001;
    double volatilityKP =
        FUNCTION.getVolatility(F, option.getStrike() + deltaK, option.getTimeToExpiry(), DATA);
    double volatilityKM =
        FUNCTION.getVolatility(F, option.getStrike() - deltaK, option.getTimeToExpiry(), DATA);
    double derivativeKK_FD = (volatilityKP + volatilityKM - 2 * volatility) / (deltaK * deltaK);
    assertEquals(derivativeKK_FD, volD2[1][1], tolerance2);
    // Derivative strike-forward
    double volatilityFPKP =
        FUNCTION.getVolatility(
            F + deltaF, option.getStrike() + deltaK, option.getTimeToExpiry(), DATA);
    double derivativeFK_FD =
        (volatilityFPKP + volatility - volatilityFP - volatilityKP) / (deltaF * deltaK);
    assertEquals(derivativeFK_FD, volD2[0][1], tolerance2);
    assertEquals(volD2[0][1], volD2[1][0], 1E-6);
  }

  private void assertEqualsRelTol(String msg, double exp, double act, double tol) {
    double delta = (Math.abs(exp) + Math.abs(act)) * tol / 2.0;
    assertEquals(act, exp, delta, msg);
  }

  @SuppressWarnings("null")
  private double fdSensitivity(
      EuropeanVanillaOption optionData,
      double forward,
      SabrFormulaData sabrData,
      SabrParameter param,
      double delta) {

    Function<SabrFormulaData, Double> funcC = null;
    Function<SabrFormulaData, Double> funcB = null;
    Function<SabrFormulaData, Double> funcA = null;
    SabrFormulaData dataC = null;
    SabrFormulaData dataB = sabrData;
    SabrFormulaData dataA = null;
    Function<SabrFormulaData, Double> func = getVolatilityFunction(optionData, forward);

    FiniteDifferenceType fdType = null;

    switch (param) {
      case Strike:
        double strike = optionData.getStrike();
        if (strike >= delta) {
          fdType = FiniteDifferenceType.CENTRAL;
          funcA = getVolatilityFunction(withStrike(optionData, strike - delta), forward);
          funcC = getVolatilityFunction(withStrike(optionData, strike + delta), forward);
        } else {
          fdType = FiniteDifferenceType.FORWARD;
          funcA = func;
          funcB = getVolatilityFunction(withStrike(optionData, strike + delta), forward);
          funcC = getVolatilityFunction(withStrike(optionData, strike + 2 * delta), forward);
        }
        dataC = sabrData;
        dataB = sabrData;
        dataA = sabrData;
        break;
      case Forward:
        if (forward > delta) {
          fdType = FiniteDifferenceType.CENTRAL;
          funcA = getVolatilityFunction(optionData, forward - delta);
          funcC = getVolatilityFunction(optionData, forward + delta);
        } else {
          fdType = FiniteDifferenceType.FORWARD;
          funcA = func;
          funcB = getVolatilityFunction(optionData, forward + delta);
          funcC = getVolatilityFunction(optionData, forward + 2 * delta);
        }
        dataC = sabrData;
        dataB = sabrData;
        dataA = sabrData;
        break;
      case Alpha:
        double a = sabrData.getAlpha();
        if (a >= delta) {
          fdType = FiniteDifferenceType.CENTRAL;
          dataA = sabrData.withAlpha(a - delta);
          dataC = sabrData.withAlpha(a + delta);
        } else {
          fdType = FiniteDifferenceType.FORWARD;
          dataA = sabrData;
          dataB = sabrData.withAlpha(a + delta);
          dataC = sabrData.withAlpha(a + 2 * delta);
        }
        funcC = func;
        funcB = func;
        funcA = func;
        break;
      case Beta:
        double b = sabrData.getBeta();
        if (b >= delta) {
          fdType = FiniteDifferenceType.CENTRAL;
          dataA = sabrData.withBeta(b - delta);
          dataC = sabrData.withBeta(b + delta);
        } else {
          fdType = FiniteDifferenceType.FORWARD;
          dataA = sabrData;
          dataB = sabrData.withBeta(b + delta);
          dataC = sabrData.withBeta(b + 2 * delta);
        }
        funcC = func;
        funcB = func;
        funcA = func;
        break;
      case Nu:
        double n = sabrData.getNu();
        if (n >= delta) {
          fdType = FiniteDifferenceType.CENTRAL;
          dataA = sabrData.withNu(n - delta);
          dataC = sabrData.withNu(n + delta);
        } else {
          fdType = FiniteDifferenceType.FORWARD;
          dataA = sabrData;
          dataB = sabrData.withNu(n + delta);
          dataC = sabrData.withNu(n + 2 * delta);
        }
        funcC = func;
        funcB = func;
        funcA = func;
        break;
      case Rho:
        double r = sabrData.getRho();
        if ((r + 1) < delta) {
          fdType = FiniteDifferenceType.FORWARD;
          dataA = sabrData;
          dataB = sabrData.withRho(r + delta);
          dataC = sabrData.withRho(r + 2 * delta);
        } else if ((1 - r) < delta) {
          fdType = FiniteDifferenceType.BACKWARD;
          dataA = sabrData.withRho(r - 2 * delta);
          dataB = sabrData.withRho(r - delta);
          dataC = sabrData;
        } else {
          fdType = FiniteDifferenceType.CENTRAL;
          dataC = sabrData.withRho(r + delta);
          dataA = sabrData.withRho(r - delta);
        }
        funcC = func;
        funcB = func;
        funcA = func;
        break;
      default:
        throw new MathException("enum not found");
    }

    if (fdType != null) {
      switch (fdType) {
        case FORWARD:
          return (-1.5 * funcA.apply(dataA) + 2.0 * funcB.apply(dataB) - 0.5 * funcC.apply(dataC))
              / delta;
        case BACKWARD:
          return (0.5 * funcA.apply(dataA) - 2.0 * funcB.apply(dataB) + 1.5 * funcC.apply(dataC))
              / delta;
        case CENTRAL:
          return (funcC.apply(dataC) - funcA.apply(dataA)) / 2.0 / delta;
        default:
          throw new MathException("enum not found");
      }
    }
    throw new MathException("enum not found");
  }

  private Function<SabrFormulaData, Double> getVolatilityFunction(
      EuropeanVanillaOption option, double forward) {
    return new Function<SabrFormulaData, Double>() {
      @Override
      public Double apply(SabrFormulaData data) {
        ArgChecker.notNull(data, "data");
        return FUNCTION.getVolatility(forward, option.getStrike(), option.getTimeToExpiry(), data);
      }
    };
  }

  private EuropeanVanillaOption withStrike(EuropeanVanillaOption option, double strike) {
    return EuropeanVanillaOption.of(strike, option.getTimeToExpiry(), option.getPutCall());
  }
}
  /**
   * Check that $\rho \simeq 1$ case is smoothly connected with a general case, i.e., comparing the
   * approximated computation and full computation around the cutoff, which is currently $\rho = 1.0
   * - 1.0e-5$ Note that the resulting numbers contain a large error if $\rho \simeq 1$ and $z
   * \simeq 1$ are true at the same time
   */
  @Test
  public void largeRhoSmoothnessTest() {
    double rhoEps = 1.e-5;
    // rhoIn is larger than the cutoff,
    // thus vol and sensitivities are computed by approximation formulas which are regular in the
    // limit rho -> 1.
    double rhoIn = 1.0 - 0.5 * rhoEps;
    // rhoOut is smaller than the cutoff, thus vol and sensitivities are computed by full formula.
    double rhoOut = 1.0 - 1.5 * rhoEps;
    SabrFormulaData dataIn = SabrFormulaData.of(ALPHA, BETA, rhoIn, NU);
    SabrFormulaData dataOut = SabrFormulaData.of(ALPHA, BETA, rhoOut, NU);

    /*
     * z<1 case, i.e., finite values in the rho->1 limit
     */
    double volatilityOut = FUNCTION.getVolatility(F, STRIKE_OTM, T, dataOut);
    double[] adjointOut = toArray(FUNCTION.getVolatilityAdjoint(F, STRIKE_OTM, T, dataOut));
    double[] volatilityDOut = new double[6];
    double[][] volatilityD2Out = new double[2][2];
    double volatility2Out =
        FUNCTION.getVolatilityAdjoint2(F, STRIKE_OTM, T, dataOut, volatilityDOut, volatilityD2Out);

    double volatilityIn = FUNCTION.getVolatility(F, STRIKE_OTM, T, dataIn);
    double[] adjointIn = toArray(FUNCTION.getVolatilityAdjoint(F, STRIKE_OTM, T, dataIn));
    double[] volatilityDIn = new double[6];
    double[][] volatilityD2In = new double[2][2];
    double volatility2In =
        FUNCTION.getVolatilityAdjoint2(F, STRIKE_OTM, T, dataIn, volatilityDIn, volatilityD2In);

    assertEquals(volatilityOut, volatilityIn, rhoEps);
    assertEquals(volatility2Out, volatility2In, rhoEps);
    for (int i = 0; i < adjointOut.length; ++i) {
      double ref = adjointOut[i];
      assertEquals(adjointOut[i], adjointIn[i], Math.max(Math.abs(ref), 1.0) * 1.e-3);
    }
    for (int i = 0; i < volatilityDOut.length; ++i) {
      double ref = volatilityDOut[i];
      assertEquals(volatilityDOut[i], volatilityDIn[i], Math.max(Math.abs(ref), 1.0) * 1.e-3);
    }

    /*
     * z>1 case, runs into infinity or 0.
     * Convergence speed is much faster (and typically smoother).
     */
    rhoIn = 1.0 - 0.999 * rhoEps;
    rhoOut = 1.0 - 1.001 * rhoEps;
    dataIn = SabrFormulaData.of(ALPHA, BETA, rhoIn, NU);
    dataOut = SabrFormulaData.of(ALPHA, BETA, rhoOut, NU);

    volatilityOut = FUNCTION.getVolatility(3.0 * F, STRIKE_ITM, T, dataOut);
    adjointOut = toArray(FUNCTION.getVolatilityAdjoint(3.0 * F, STRIKE_ITM, T, dataOut));
    volatilityDOut = new double[6];
    volatilityD2Out = new double[2][2];
    volatility2Out =
        FUNCTION.getVolatilityAdjoint2(
            3.0 * F, STRIKE_ITM, T, dataOut, volatilityDOut, volatilityD2Out);

    volatilityIn = FUNCTION.getVolatility(3.0 * F, STRIKE_ITM, T, dataIn);
    adjointIn = toArray(FUNCTION.getVolatilityAdjoint(3.0 * F, STRIKE_ITM, T, dataIn));
    volatilityDIn = new double[6];
    volatilityD2In = new double[2][2];
    volatility2In =
        FUNCTION.getVolatilityAdjoint2(
            3.0 * F, STRIKE_ITM, T, dataIn, volatilityDIn, volatilityD2In);

    assertEquals(volatilityOut, volatilityIn, rhoEps);
    assertEquals(volatility2Out, volatility2In, rhoEps);
    for (int i = 0; i < adjointOut.length; ++i) {
      double ref = adjointOut[i];
      assertEquals(ref, adjointIn[i], Math.max(Math.abs(ref), 1.0) * 1.e-2);
    }
    for (int i = 0; i < volatilityDOut.length; ++i) {
      double ref = volatilityDOut[i];
      assertEquals(ref, volatilityDIn[i], Math.max(Math.abs(ref), 1.0) * 1.e-2);
    }
  }
 /**
  * Computes the first and second order derivatives of the Black implied volatility in the SABR
  * model.
  *
  * <p>The first derivative values will be stored in the input array {@code volatilityD} The array
  * contains, [0] Derivative w.r.t the forward, [1] the derivative w.r.t the strike, [2] the
  * derivative w.r.t. to alpha, [3] the derivative w.r.t. to beta, [4] the derivative w.r.t. to
  * rho, and [5] the derivative w.r.t. to nu. Thus the length of the array should be 6.
  *
  * <p>The second derivative values will be stored in the input array {@code volatilityD2}. Only
  * the second order derivative with respect to the forward and strike are implemented. The array
  * contains [0][0] forward-forward; [0][1] forward-strike; [1][1] strike-strike. Thus the size
  * should be 2 x 2.
  *
  * <p>Around ATM, a first order expansion is used to due to some 0/0-type indetermination. The
  * second order derivative produced is poor around ATM.
  *
  * @param forward the forward value of the underlying
  * @param strike the strike value of the option
  * @param timeToExpiry the time to expiry of the option
  * @param data the SABR data.
  * @param volatilityD the array used to return the first order derivative
  * @param volatilityD2 the array of array used to return the second order derivative
  * @return the Black implied volatility
  */
 @Override
 public double volatilityAdjoint2(
     double forward,
     double strike,
     double timeToExpiry,
     SabrFormulaData data,
     double[] volatilityD,
     double[][] volatilityD2) {
   double k = Math.max(strike, 0.000001);
   double alpha = data.getAlpha();
   double beta = data.getBeta();
   double rho = data.getRho();
   double nu = data.getNu();
   // Forward
   double h0 = (1 - beta) / 2;
   double h1 = forward * k;
   double h1h0 = Math.pow(h1, h0);
   double h12 = h1h0 * h1h0;
   double h2 = Math.log(forward / k);
   double h22 = h2 * h2;
   double h23 = h22 * h2;
   double h24 = h23 * h2;
   double f1 = h1h0 * (1 + h0 * h0 / 6.0 * (h22 + h0 * h0 / 20.0 * h24));
   double f2 = nu / alpha * h1h0 * h2;
   double f3 =
       h0 * h0 / 6.0 * alpha * alpha / h12
           + rho * beta * nu * alpha / 4.0 / h1h0
           + (2 - 3 * rho * rho) / 24.0 * nu * nu;
   double sqrtf2 = Math.sqrt(1 - 2 * rho * f2 + f2 * f2);
   double f2x = 0.0;
   double x = 0.0, xp = 0, xpp = 0;
   if (DoubleMath.fuzzyEquals(f2, 0.0, SMALL_Z)) {
     f2x = 1.0 - 0.5 * f2 * rho; // small f2 expansion to f2^2 terms
   } else {
     if (DoubleMath.fuzzyEquals(rho, 1.0, RHO_EPS)) {
       x =
           f2 < 1.0
               ? -Math.log(1.0 - f2) - 0.5 * Math.pow(f2 / (f2 - 1.0), 2) * (1.0 - rho)
               : Math.log(2.0 * f2 - 2.0) - Math.log(1.0 - rho);
     } else {
       x = Math.log((sqrtf2 + f2 - rho) / (1 - rho));
     }
     xp = 1. / sqrtf2;
     xpp = (rho - f2) / Math.pow(sqrtf2, 3.0);
     f2x = f2 / x;
   }
   double sigma = Math.max(MIN_VOL, alpha / f1 * f2x * (1 + f3 * timeToExpiry));
   // First level
   double h0Dbeta = -0.5;
   double sigmaDf1 = -sigma / f1;
   double sigmaDf2 = 0;
   if (DoubleMath.fuzzyEquals(f2, 0.0, SMALL_Z)) {
     sigmaDf2 = alpha / f1 * (1 + f3 * timeToExpiry) * -0.5 * rho;
   } else {
     sigmaDf2 = alpha / f1 * (1 + f3 * timeToExpiry) * (1.0 / x - f2 * xp / (x * x));
   }
   double sigmaDf3 = alpha / f1 * f2x * timeToExpiry;
   double sigmaDf4 = f2x / f1 * (1 + f3 * timeToExpiry);
   double sigmaDx = -alpha / f1 * f2 / (x * x) * (1 + f3 * timeToExpiry);
   double[][] sigmaD2ff = new double[3][3];
   sigmaD2ff[0][0] = -sigmaDf1 / f1 + sigma / (f1 * f1); // OK
   sigmaD2ff[0][1] = -sigmaDf2 / f1;
   sigmaD2ff[0][2] = -sigmaDf3 / f1;
   if (DoubleMath.fuzzyEquals(f2, 0.0, SMALL_Z)) {
     sigmaD2ff[1][2] = alpha / f1 * -0.5 * rho * timeToExpiry;
   } else {
     sigmaD2ff[1][1] =
         alpha
             / f1
             * (1 + f3 * timeToExpiry)
             * (-2 * xp / (x * x) - f2 * xpp / (x * x) + 2 * f2 * xp * xp / (x * x * x));
     sigmaD2ff[1][2] = alpha / f1 * timeToExpiry * (1.0 / x - f2 * xp / (x * x));
   }
   sigmaD2ff[2][2] = 0.0;
   //      double sigma = alpha / f1 * f2x * (1 + f3 * theta);
   // Second level
   double[] f1Dh = new double[3];
   double[] f2Dh = new double[3];
   double[] f3Dh = new double[3];
   f1Dh[0] = h1h0 * (h0 * (h22 / 3.0 + h0 * h0 / 40.0 * h24)) + Math.log(h1) * f1;
   f1Dh[1] = h0 * f1 / h1;
   f1Dh[2] = h1h0 * (h0 * h0 / 6.0 * (2.0 * h2 + h0 * h0 / 5.0 * h23));
   f2Dh[0] = Math.log(h1) * f2;
   f2Dh[1] = h0 * f2 / h1;
   f2Dh[2] = nu / alpha * h1h0;
   f3Dh[0] =
       h0 / 3.0 * alpha * alpha / h12
           - 2 * h0 * h0 / 6.0 * alpha * alpha / h12 * Math.log(h1)
           - rho * beta * nu * alpha / 4.0 / h1h0 * Math.log(h1);
   f3Dh[1] =
       -2 * h0 * h0 / 6.0 * alpha * alpha / h12 * h0 / h1
           - rho * beta * nu * alpha / 4.0 / h1h0 * h0 / h1;
   f3Dh[2] = 0.0;
   double[] f1Dp = new double[4]; // Derivative to sabr parameters
   double[] f2Dp = new double[4];
   double[] f3Dp = new double[4];
   double[] f4Dp = new double[4];
   f1Dp[0] = 0.0;
   f1Dp[1] = f1Dh[0] * h0Dbeta;
   f1Dp[2] = 0.0;
   f1Dp[3] = 0.0;
   f2Dp[0] = -f2 / alpha;
   f2Dp[1] = f2Dh[0] * h0Dbeta;
   f2Dp[2] = 0.0;
   f2Dp[3] = h1h0 * h2 / alpha;
   f3Dp[0] = h0 * h0 / 3.0 * alpha / h12 + rho * beta * nu / 4.0 / h1h0;
   f3Dp[1] = rho * nu * alpha / 4.0 / h1h0 + f3Dh[0] * h0Dbeta;
   f3Dp[2] = beta * nu * alpha / 4.0 / h1h0 - rho / 4.0 * nu * nu;
   f3Dp[3] = rho * beta * alpha / 4.0 / h1h0 + (2 - 3 * rho * rho) / 12.0 * nu;
   f4Dp[0] = 1.0;
   f4Dp[1] = 0.0;
   f4Dp[2] = 0.0;
   f4Dp[3] = 0.0;
   double sigmaDh1 = sigmaDf1 * f1Dh[1] + sigmaDf2 * f2Dh[1] + sigmaDf3 * f3Dh[1];
   double sigmaDh2 = sigmaDf1 * f1Dh[2] + sigmaDf2 * f2Dh[2] + sigmaDf3 * f3Dh[2];
   double[][] f1D2hh = new double[2][2]; // No h0
   double[][] f2D2hh = new double[2][2];
   double[][] f3D2hh = new double[2][2];
   f1D2hh[0][0] = h0 * (h0 - 1) * f1 / (h1 * h1);
   f1D2hh[0][1] = h0 * h1h0 / h1 * h0 * h0 / 6.0 * (2.0 * h2 + 4.0 * h0 * h0 / 20.0 * h23);
   f1D2hh[1][1] = h1h0 * (h0 * h0 / 6.0 * (2.0 + 12.0 * h0 * h0 / 20.0 * h2));
   f2D2hh[0][0] = h0 * (h0 - 1) * f2 / (h1 * h1);
   f2D2hh[0][1] = nu / alpha * h0 * h1h0 / h1;
   f2D2hh[1][1] = 0.0;
   f3D2hh[0][0] =
       2 * h0 * (2 * h0 + 1) * h0 * h0 / 6.0 * alpha * alpha / (h12 * h1 * h1)
           + h0 * (h0 + 1) * rho * beta * nu * alpha / 4.0 / (h1h0 * h1 * h1);
   f3D2hh[0][1] = 0.0;
   f3D2hh[1][1] = 0.0;
   double[][] sigmaD2hh = new double[2][2]; // No h0
   for (int loopx = 0; loopx < 2; loopx++) {
     for (int loopy = loopx; loopy < 2; loopy++) {
       sigmaD2hh[loopx][loopy] =
           (sigmaD2ff[0][0] * f1Dh[loopy + 1]
                       + sigmaD2ff[0][1] * f2Dh[loopy + 1]
                       + sigmaD2ff[0][2] * f3Dh[loopy + 1])
                   * f1Dh[loopx + 1]
               + sigmaDf1 * f1D2hh[loopx][loopy]
               + (sigmaD2ff[0][1] * f1Dh[loopy + 1]
                       + sigmaD2ff[1][1] * f2Dh[loopy + 1]
                       + sigmaD2ff[1][2] * f3Dh[loopy + 1])
                   * f2Dh[loopx + 1]
               + sigmaDf2 * f2D2hh[loopx][loopy]
               + (sigmaD2ff[0][2] * f1Dh[loopy + 1]
                       + sigmaD2ff[1][2] * f2Dh[loopy + 1]
                       + sigmaD2ff[2][2] * f3Dh[loopy + 1])
                   * f3Dh[loopx + 1]
               + sigmaDf3 * f3D2hh[loopx][loopy];
     }
   }
   // Third level
   double h1Df = k;
   double h1Dk = forward;
   double h1D2ff = 0.0;
   double h1D2kf = 1.0;
   double h1D2kk = 0.0;
   double h2Df = 1.0 / forward;
   double h2Dk = -1.0 / k;
   double h2D2ff = -1 / (forward * forward);
   double h2D2fk = 0.0;
   double h2D2kk = 1.0 / (k * k);
   volatilityD[0] = sigmaDh1 * h1Df + sigmaDh2 * h2Df;
   volatilityD[1] = sigmaDh1 * h1Dk + sigmaDh2 * h2Dk;
   volatilityD[2] =
       sigmaDf1 * f1Dp[0] + sigmaDf2 * f2Dp[0] + sigmaDf3 * f3Dp[0] + sigmaDf4 * f4Dp[0];
   volatilityD[3] =
       sigmaDf1 * f1Dp[1] + sigmaDf2 * f2Dp[1] + sigmaDf3 * f3Dp[1] + sigmaDf4 * f4Dp[1];
   if (DoubleMath.fuzzyEquals(f2, 0.0, SMALL_Z)) {
     volatilityD[4] = -0.5 * f2 + sigmaDf3 * f3Dp[2];
   } else {
     double xDr;
     if (DoubleMath.fuzzyEquals(rho, 1.0, RHO_EPS)) {
       xDr =
           f2 > 1.0
               ? 1.0 / (1.0 - rho) + (0.5 - f2) / (f2 - 1.0) / (f2 - 1.0)
               : 0.5 * Math.pow(f2 / (1.0 - f2), 2.0)
                   + 0.25 * (f2 - 4.0) * Math.pow(f2 / (f2 - 1.0), 3) / (f2 - 1.0) * (1.0 - rho);
       if (Doubles.isFinite(xDr)) {
         volatilityD[4] =
             sigmaDf1 * f1Dp[2] + sigmaDx * xDr + sigmaDf3 * f3Dp[2] + sigmaDf4 * f4Dp[2];
       } else {
         volatilityD[4] = Double.NEGATIVE_INFINITY;
       }
     } else {
       xDr = (-f2 / sqrtf2 - 1 + (sqrtf2 + f2 - rho) / (1 - rho)) / (sqrtf2 + f2 - rho);
       volatilityD[4] =
           sigmaDf1 * f1Dp[2] + sigmaDx * xDr + sigmaDf3 * f3Dp[2] + sigmaDf4 * f4Dp[2];
     }
   }
   volatilityD[5] =
       sigmaDf1 * f1Dp[3] + sigmaDf2 * f2Dp[3] + sigmaDf3 * f3Dp[3] + sigmaDf4 * f4Dp[3];
   volatilityD2[0][0] =
       (sigmaD2hh[0][0] * h1Df + sigmaD2hh[0][1] * h2Df) * h1Df
           + sigmaDh1 * h1D2ff
           + (sigmaD2hh[0][1] * h1Df + sigmaD2hh[1][1] * h2Df) * h2Df
           + sigmaDh2 * h2D2ff;
   volatilityD2[0][1] =
       (sigmaD2hh[0][0] * h1Dk + sigmaD2hh[0][1] * h2Dk) * h1Df
           + sigmaDh1 * h1D2kf
           + (sigmaD2hh[0][1] * h1Dk + sigmaD2hh[1][1] * h2Dk) * h2Df
           + sigmaDh2 * h2D2fk;
   volatilityD2[1][0] = volatilityD2[0][1];
   volatilityD2[1][1] =
       (sigmaD2hh[0][0] * h1Dk + sigmaD2hh[0][1] * h2Dk) * h1Dk
           + sigmaDh1 * h1D2kk
           + (sigmaD2hh[0][1] * h1Dk + sigmaD2hh[1][1] * h2Dk) * h2Dk
           + sigmaDh2 * h2D2kk;
   return sigma;
 }