/**
   * Calculates the present value sensitivity to the implied volatility of the swaption product.
   *
   * <p>The sensitivity to the implied normal volatility is also called normal vega.
   *
   * @param swaption the swaption product
   * @param ratesProvider the rates provider
   * @param volatilityProvider the normal volatility provider
   * @return the point sensitivity to the normal volatility
   */
  public SwaptionSensitivity presentValueSensitivityNormalVolatility(
      SwaptionProduct swaption,
      RatesProvider ratesProvider,
      NormalVolatilitySwaptionProvider volatilityProvider) {

    ExpandedSwaption expanded = swaption.expand();
    validate(ratesProvider, expanded, volatilityProvider);
    ZonedDateTime expiryDateTime = expanded.getExpiryDateTime();
    double expiry = volatilityProvider.relativeTime(expiryDateTime);
    ExpandedSwap underlying = expanded.getUnderlying();
    ExpandedSwapLeg fixedLeg = fixedLeg(underlying);
    double tenor = volatilityProvider.tenor(fixedLeg.getStartDate(), fixedLeg.getEndDate());
    double pvbp = swapPricer.getLegPricer().pvbp(fixedLeg, ratesProvider);
    double strike = swapPricer.getLegPricer().couponEquivalent(fixedLeg, ratesProvider, pvbp);
    if (expiry < 0.0d) { // Option has expired already
      return SwaptionSensitivity.of(
          volatilityProvider.getConvention(),
          expiryDateTime,
          tenor,
          strike,
          0.0d,
          fixedLeg.getCurrency(),
          0.0d);
    }
    double forward = swapPricer.parRate(underlying, ratesProvider);
    double volatility = volatilityProvider.getVolatility(expiryDateTime, tenor, strike, forward);
    NormalFunctionData normalData = NormalFunctionData.of(forward, Math.abs(pvbp), volatility);
    boolean isCall = (fixedLeg.getPayReceive() == PayReceive.PAY);
    // Payer at strike is exercise when rate > strike, i.e. call on rate
    EuropeanVanillaOption option =
        EuropeanVanillaOption.of(strike, expiry, isCall ? PutCall.CALL : PutCall.PUT);
    // option required to pass the strike (in case the swap has non-constant coupon).
    // Backward sweep
    double vega =
        NORMAL.getVega(option, normalData)
            * ((expanded.getLongShort() == LongShort.LONG) ? 1.0 : -1.0);
    return SwaptionSensitivity.of(
        volatilityProvider.getConvention(),
        expiryDateTime,
        tenor,
        strike,
        forward,
        fixedLeg.getCurrency(),
        vega);
  }
 public void test_volatility_sensitivity() {
   double eps = 1.0e-6;
   int nData = TIME.size();
   for (int i = 0; i < NB_TEST; i++) {
     SwaptionSensitivity point =
         SwaptionSensitivity.of(
             CONVENTION,
             TEST_OPTION_EXPIRY[i],
             TENOR.get(i),
             TEST_STRIKE,
             TEST_FORWARD,
             GBP,
             TEST_SENSITIVITY[i]);
     SurfaceCurrencyParameterSensitivity sensi =
         PROVIDER_WITH_PARAM.surfaceCurrencyParameterSensitivity(point);
     Map<DoublesPair, Double> map = new HashMap<DoublesPair, Double>();
     for (int j = 0; j < nData; ++j) {
       DoubleArray volDataUp = VOL.subArray(0, nData).with(j, VOL.get(j) + eps);
       DoubleArray volDataDw = VOL.subArray(0, nData).with(j, VOL.get(j) - eps);
       InterpolatedNodalSurface paramUp =
           InterpolatedNodalSurface.of(
               METADATA_WITH_PARAM, TIME, TENOR, volDataUp, INTERPOLATOR_2D);
       InterpolatedNodalSurface paramDw =
           InterpolatedNodalSurface.of(
               METADATA_WITH_PARAM, TIME, TENOR, volDataDw, INTERPOLATOR_2D);
       BlackVolatilityExpiryTenorSwaptionProvider provUp =
           BlackVolatilityExpiryTenorSwaptionProvider.of(
               paramUp, CONVENTION, ACT_365F, VALUATION_DATE_TIME);
       BlackVolatilityExpiryTenorSwaptionProvider provDw =
           BlackVolatilityExpiryTenorSwaptionProvider.of(
               paramDw, CONVENTION, ACT_365F, VALUATION_DATE_TIME);
       double volUp =
           provUp.getVolatility(TEST_OPTION_EXPIRY[i], TEST_TENOR[i], TEST_STRIKE, TEST_FORWARD);
       double volDw =
           provDw.getVolatility(TEST_OPTION_EXPIRY[i], TEST_TENOR[i], TEST_STRIKE, TEST_FORWARD);
       double fd = 0.5 * (volUp - volDw) / eps;
       map.put(DoublesPair.of(TIME.get(j), TENOR.get(j)), fd);
     }
     SurfaceCurrencyParameterSensitivity sensiFromNoMetadata =
         PROVIDER.surfaceCurrencyParameterSensitivity(point);
     List<SurfaceParameterMetadata> list = sensi.getMetadata().getParameterMetadata().get();
     DoubleArray computed = sensi.getSensitivity();
     assertEquals(computed.size(), nData);
     for (int j = 0; j < list.size(); ++j) {
       SwaptionSurfaceExpiryTenorNodeMetadata metadata =
           (SwaptionSurfaceExpiryTenorNodeMetadata) list.get(i);
       double expected = map.get(DoublesPair.of(metadata.getYearFraction(), metadata.getTenor()));
       assertEquals(computed.get(i), expected, eps);
       assertTrue(
           sensiFromNoMetadata.getMetadata().getParameterMetadata().get().contains(metadata));
     }
   }
 }