protected double getResidual(
      final double fwd, final double expiry, final double[] ks, final double[] vols) {

    // Check for trivial case where cutoff is so low that there's no effective value in the option
    final double cutoffPrice =
        BlackFormulaRepository.price(fwd, ks[0], expiry, vols[0], ks[0] > fwd);
    if (CompareUtils.closeEquals(cutoffPrice, 0)) {
      return 0.0; // i.e. the tail function is never used
    }
    // The typical case - fit a  ShiftedLognormal to the two strike-vol pairs
    final ShiftedLognormalVolModel leftExtrapolator =
        new ShiftedLognormalVolModel(fwd, expiry, ks[0], vols[0], ks[1], vols[1]);

    // Now, handle behaviour near zero strike. ShiftedLognormalVolModel has non-zero put price for
    // zero strike.
    // What we do is to find the strike, k_min, at which f(k) = p(k)/k^2 begins to blow up, by
    // finding the minimum of this function, k_min
    // then setting f(k) = f(k_min) for k < k_min. This ensures the implied volatility and the
    // integrand are well behaved in the limit k -> 0.
    final Function1D<Double, Double> shiftedLnIntegrand =
        new Function1D<Double, Double>() {
          @Override
          public Double evaluate(final Double strike) {
            return leftExtrapolator.priceFromFixedStrike(strike) / (strike * strike);
          }
        };
    final double kMin = new BrentMinimizer1D().minimize(shiftedLnIntegrand, EPS, EPS, ks[0]);
    final double fMin = shiftedLnIntegrand.evaluate(kMin);
    double res = fMin * kMin; // the (hopefully) very small rectangular bit between zero and kMin

    res += _integrator.integrate(shiftedLnIntegrand, kMin, ks[0]);

    return res;
  }
 private double presentValueStandard(
     final double forward,
     final double strike,
     final double expiry,
     final boolean isCall,
     final double df,
     final double notional,
     final double yearFraction) {
   double volatility = _smileFunction.getVolatility(strike);
   double price =
       BlackFormulaRepository.price(forward, strike, expiry, volatility, isCall)
           * df
           * notional
           * yearFraction;
   return price;
 }
  /** The dividend is cash or proportional to asset price */
  @Test
  public void priceDiscreteDividendTest() {
    final LatticeSpecification[] lattices =
        new LatticeSpecification[] {
          new CoxRossRubinsteinLatticeSpecification(),
          new JarrowRuddLatticeSpecification(),
          new TrigeorgisLatticeSpecification(),
          new JabbourKraminYoungLatticeSpecification(),
          new TianLatticeSpecification(),
          new LeisenReimerLatticeSpecification()
        };

    final double[] propDividends = new double[] {0.01, 0.01, 0.01};
    final double[] cashDividends = new double[] {5., 10., 8.};
    final double[] dividendTimes = new double[] {TIME / 9., TIME / 3., TIME / 2.};

    final boolean[] tfSet = new boolean[] {true, false};
    for (final LatticeSpecification lattice : lattices) {
      for (final boolean isCall : tfSet) {
        for (final double strike : STRIKES) {
          for (final double interest : INTERESTS) {
            for (final double vol : VOLS) {
              final int[] choicesSteps = new int[] {33, 115};
              for (final int nSteps : choicesSteps) {
                final OptionFunctionProvider1D function =
                    new EuropeanVanillaOptionFunctionProvider(strike, TIME, nSteps, isCall);
                final DividendFunctionProvider cashDividend =
                    new CashDividendFunctionProvider(dividendTimes, cashDividends);
                final DividendFunctionProvider propDividend =
                    new ProportionalDividendFunctionProvider(dividendTimes, propDividends);
                final double df = Math.exp(-interest * TIME);
                final double resSpot =
                    SPOT
                        * Math.exp(interest * TIME)
                        * (1. - propDividends[0])
                        * (1. - propDividends[1])
                        * (1. - propDividends[2]);
                final double modSpot =
                    SPOT
                        - cashDividends[0] * Math.exp(-interest * dividendTimes[0])
                        - cashDividends[1] * Math.exp(-interest * dividendTimes[1])
                        - cashDividends[2] * Math.exp(-interest * dividendTimes[2]);
                final double exactProp =
                    df * BlackFormulaRepository.price(resSpot, strike, TIME, vol, isCall);
                final double appCash =
                    BlackScholesFormulaRepository.price(
                        modSpot, strike, TIME, vol, interest, interest, isCall);
                final double resProp =
                    _model.getPrice(lattice, function, SPOT, vol, interest, propDividend);
                final double refProp = Math.max(exactProp, 1.) / Math.sqrt(nSteps);
                assertEquals(resProp, exactProp, refProp);
                final double resCash =
                    _model.getPrice(lattice, function, SPOT, vol, interest, cashDividend);
                final double refCash = Math.max(appCash, 1.) / Math.sqrt(nSteps);
                assertEquals(resCash, appCash, refCash);

                if (lattice instanceof CoxRossRubinsteinLatticeSpecification
                    || lattice instanceof JarrowRuddLatticeSpecification
                    || lattice instanceof TrigeorgisLatticeSpecification
                    || lattice instanceof TianLatticeSpecification) {
                  final double resPropTrinomial =
                      _modelTrinomial.getPrice(
                          lattice, function, SPOT, vol, interest, propDividend);
                  final double resCashTrinomial =
                      _modelTrinomial.getPrice(
                          lattice, function, SPOT, vol, interest, cashDividend);
                  assertEquals(
                      resPropTrinomial, resProp, Math.max(resProp, 1.) / Math.sqrt(nSteps));
                  assertEquals(
                      resCashTrinomial, resCash, Math.max(resCash, 1.) / Math.sqrt(nSteps));
                }
              }
            }
          }
        }
      }
    }
  }
  /** Check trivial extrapolation is recovered for Benaim-Dodgson-Kainth extrapolation */
  @Test
  public void functionRecoveryBDKExtrapolationTest() {
    double forward = 1.0;
    double expiry = 3.0;
    int nSamples = 4;
    double[] strikes = new double[nSamples];
    double[] vols = new double[nSamples];

    final double mu = 1.0;
    final double a = -1.0;
    final double b = 0.0;
    final double c = 0.0;

    // Expected left extrapolation
    Function1D<Double, Double> left =
        new Function1D<Double, Double>() {
          @Override
          public Double evaluate(Double strike) {
            return Math.pow(strike, mu) * Math.exp(a + b * strike + c * strike * strike);
          }
        };
    // Expected right extrapolation
    Function1D<Double, Double> right =
        new Function1D<Double, Double>() {
          @Override
          public Double evaluate(Double strike) {
            return Math.pow(strike, -mu) * Math.exp(a + b / strike + c / strike / strike);
          }
        };

    for (int i = 0; i < nSamples; ++i) {
      double strike = forward * (0.75 + 0.05 * i);
      double price = left.evaluate(strike);
      double vol = BlackFormulaRepository.impliedVolatility(price, forward, strike, expiry, false);
      strikes[i] = strike;
      vols[i] = vol;
    }
    SmileExtrapolationFunctionSABRProvider extrapBDK =
        new BenaimDodgsonKainthExtrapolationFunctionProvider(mu, mu);
    SmileInterpolatorSABRWithExtrapolation interpBDK =
        new SmileInterpolatorSABRWithExtrapolation(
            new SABRBerestyckiVolatilityFunction(), extrapBDK);
    InterpolatedSmileFunction funcBDK =
        new InterpolatedSmileFunction(interpBDK, forward, strikes, expiry, vols);
    double[] keys = new double[] {forward * 0.1, forward * 0.5, forward * 0.66};
    for (int i = 0; i < keys.length; ++i) {
      double vol = funcBDK.getVolatility(keys[i]);
      double price = BlackFormulaRepository.price(forward, keys[i], expiry, vol, false);
      assertEquals(left.evaluate(keys[i]), price, 1.e-2);
    }

    for (int i = 0; i < nSamples; ++i) {
      double strike = forward * (1.1 + 0.05 * i);
      double price = right.evaluate(strike);
      double vol = BlackFormulaRepository.impliedVolatility(price, forward, strike, expiry, true);
      strikes[i] = strike;
      vols[i] = vol;
    }
    extrapBDK = new BenaimDodgsonKainthExtrapolationFunctionProvider(mu, mu);
    interpBDK = new SmileInterpolatorSABRWithExtrapolation(extrapBDK);
    funcBDK = new InterpolatedSmileFunction(interpBDK, forward, strikes, expiry, vols);
    keys = new double[] {forward * 1.31, forward * 1.5, forward * 2.61, forward * 15.0};
    for (int i = 0; i < keys.length; ++i) {
      double vol = funcBDK.getVolatility(keys[i]);
      double price = BlackFormulaRepository.price(forward, keys[i], expiry, vol, true);
      assertEquals(right.evaluate(keys[i]), price, 1.e-2);
    }
  }
  /** Check C2 smoothness of Benaim-Dodgson-Kainth extrapolation */
  @Test
  public void BDKSmoothnessGeneralTest() {
    double eps = 1.0e-5;

    double expiry = 1.5;
    double forward = 1.1;
    int nStrikes = 10;
    double[] strikes = new double[nStrikes];
    double[] impliedVols =
        new double[] {0.97, 0.92, 0.802, 0.745, 0.781, 0.812, 0.8334, 0.878, 0.899, 0.9252};
    for (int i = 0; i < nStrikes; ++i) {
      strikes[i] = forward * (0.85 + i * 0.05);
    }

    double muLow =
        strikes[0]
            * BlackFormulaRepository.dualDelta(forward, strikes[0], expiry, impliedVols[0], false)
            / BlackFormulaRepository.price(forward, strikes[0], expiry, impliedVols[0], false);
    double muHigh =
        -strikes[nStrikes - 1]
            * BlackFormulaRepository.dualDelta(
                forward, strikes[nStrikes - 1], expiry, impliedVols[nStrikes - 1], true)
            / BlackFormulaRepository.price(
                forward, strikes[nStrikes - 1], expiry, impliedVols[nStrikes - 1], true);

    SmileExtrapolationFunctionSABRProvider extrapBDK =
        new BenaimDodgsonKainthExtrapolationFunctionProvider(muLow, muHigh);
    SmileInterpolatorSABRWithExtrapolation interpBDK =
        new SmileInterpolatorSABRWithExtrapolation(
            new SABRBerestyckiVolatilityFunction(), extrapBDK);
    InterpolatedSmileFunction funcBDK =
        new InterpolatedSmileFunction(interpBDK, forward, strikes, expiry, impliedVols);

    List<SABRFormulaData> modelParams =
        (new SmileInterpolatorSABR())
            .getFittedModelParameters(forward, strikes, expiry, impliedVols);
    SABRExtrapolationLeftFunction sabrLeftExtrapolation =
        new SABRExtrapolationLeftFunction(
            forward,
            modelParams.get(0),
            strikes[0],
            expiry,
            muLow,
            new SABRHaganVolatilityFunction());
    SABRExtrapolationRightFunction sabrRightExtrapolation =
        new SABRExtrapolationRightFunction(
            forward,
            modelParams.get(nStrikes - 3),
            strikes[nStrikes - 1],
            expiry,
            muHigh,
            new SABRHaganVolatilityFunction());

    /*
     * left interpolation
     */
    {
      // Checking underlying extrapolation
      double boundaryValue =
          sabrLeftExtrapolation.price(new EuropeanVanillaOption(strikes[0], expiry, false));
      double CutoffUp = strikes[0] + eps;
      double CutoffDw = strikes[0] - eps;
      double optionPriceExt =
          sabrLeftExtrapolation.price(new EuropeanVanillaOption(CutoffDw, expiry, false));
      double optionPriceInt =
          sabrLeftExtrapolation.price(new EuropeanVanillaOption(CutoffUp, expiry, false));
      assertEquals(boundaryValue, optionPriceExt, eps);
      assertEquals(boundaryValue, optionPriceInt, eps);
      double optionPriceExtDw =
          sabrLeftExtrapolation.price(new EuropeanVanillaOption(CutoffDw - eps, expiry, false));
      double firstExt = (1.5 * boundaryValue + 0.5 * optionPriceExtDw - 2.0 * optionPriceExt) / eps;
      double optionPriceIntUp =
          sabrLeftExtrapolation.price(new EuropeanVanillaOption(CutoffUp + eps, expiry, false));
      double firstInt = (2.0 * optionPriceInt - 0.5 * optionPriceIntUp - 1.5 * boundaryValue) / eps;
      assertEquals(firstInt, firstExt, eps);
      double secondExt = (boundaryValue + optionPriceExtDw - 2.0 * optionPriceExt) / eps / eps;
      double secondInt = (optionPriceIntUp + boundaryValue - 2.0 * optionPriceInt) / eps / eps;
      assertEquals(secondInt, secondExt, Math.abs(secondInt) * 1.0e-3);

      // Checking volatility function
      double volInt = funcBDK.getVolatility(CutoffUp);
      double volExt = funcBDK.getVolatility(CutoffDw);
      double volBoundary = funcBDK.getVolatility(strikes[0]);
      assertEquals(volBoundary, volInt, eps);
      assertEquals(volBoundary, volExt, eps);
      double volExtDw = funcBDK.getVolatility(CutoffDw - eps);
      double volFirstExt = (1.5 * volBoundary + 0.5 * volExtDw - 2.0 * volExt) / eps;
      double volIntUp = funcBDK.getVolatility(CutoffUp + eps);
      double volFirstInt = (2.0 * volInt - 0.5 * volIntUp - 1.5 * volBoundary) / eps;
      assertEquals(volFirstInt, volFirstExt, eps);
      double volSecondExt = (volBoundary + volExtDw - 2.0 * volExt) / eps / eps;
      double volSecondInt = (volIntUp + volBoundary - 2.0 * volInt) / eps / eps;
      assertEquals(volSecondInt, volSecondExt, Math.abs(volSecondInt) * 1.0e-3);
    }

    /*
     * right interpolation
     */
    {
      // Checking underlying extrapolation
      double boundaryValue =
          sabrRightExtrapolation.price(
              new EuropeanVanillaOption(strikes[nStrikes - 1], expiry, true));
      double CutoffUp = strikes[nStrikes - 1] + eps;
      double CutoffDw = strikes[nStrikes - 1] - eps;
      double optionPriceExt =
          sabrRightExtrapolation.price(new EuropeanVanillaOption(CutoffUp, expiry, true));
      double optionPriceInt =
          sabrRightExtrapolation.price(new EuropeanVanillaOption(CutoffDw, expiry, true));
      assertEquals(boundaryValue, optionPriceExt, eps);
      assertEquals(boundaryValue, optionPriceInt, eps);
      double optionPriceExtUp =
          sabrRightExtrapolation.price(new EuropeanVanillaOption(CutoffUp + eps, expiry, true));
      double firstExt = (2.0 * optionPriceExt - 0.5 * optionPriceExtUp - 1.5 * boundaryValue) / eps;
      double optionPriceIntDw =
          sabrRightExtrapolation.price(new EuropeanVanillaOption(CutoffDw - eps, expiry, true));
      double firstInt =
          (-2.0 * optionPriceInt + 1.5 * boundaryValue + 0.5 * optionPriceIntDw) / eps;
      assertEquals(firstInt, firstExt, eps);
      double secondExt = (optionPriceExtUp + boundaryValue - 2.0 * optionPriceExt) / eps / eps;
      double secondInt = (boundaryValue + optionPriceIntDw - 2.0 * optionPriceInt) / eps / eps;
      assertEquals(secondInt, secondExt, Math.abs(secondInt) * 1.0e-3);

      // Checking volatility function
      double volInt = funcBDK.getVolatility(CutoffDw);
      double volExt = funcBDK.getVolatility(CutoffUp);
      double volBoundary = funcBDK.getVolatility(strikes[nStrikes - 1]);
      assertEquals(volBoundary, volInt, eps);
      assertEquals(volBoundary, volExt, eps);
      double volExtUp = funcBDK.getVolatility(CutoffUp + eps);
      double volFirstExt = (2.0 * volExt - 0.5 * volExtUp - 1.5 * volBoundary) / eps;
      double volIntDw = funcBDK.getVolatility(CutoffDw - eps);
      double volFirstInt = (-2.0 * volInt + 1.5 * volBoundary + 0.5 * volIntDw) / eps;
      assertEquals(volFirstInt, volFirstExt, eps);
      double volSecondExt = (volBoundary + volExtUp - 2.0 * volExt) / eps / eps;
      double volSecondInt = (volIntDw + volBoundary - 2.0 * volInt) / eps / eps;
      assertEquals(volSecondInt, volSecondExt, Math.abs(volSecondInt) * 1.0e-3);
    }
  }
  @SuppressWarnings("unused")
  @Test
  public void limitTest() {
    final LocalDate optionExpiry = getNextIMMDate(TRADE_DATE).minusDays(1);
    final double timeToExpiry = ACT365F.yearFraction(TRADE_DATE, optionExpiry);
    final CDSAnalytic fwdCDX = FACTORY.makeCDX(optionExpiry, Period.ofYears(5));
    final CDSAnalytic fwdStartingCDX = fwdCDX.withOffset(timeToExpiry);

    final double[] indexPUF = new double[] {0.0556, 0.0582, 0.0771, 0.0652};
    final CDSAnalytic[] indexCDS = FACTORY.makeCDX(TRADE_DATE, INDEX_PILLARS);

    final IntrinsicIndexDataBundle adjCurves =
        PSA.adjustCurves(indexPUF, indexCDS, INDEX_COUPON, YIELD_CURVE, INTRINSIC_DATA);
    final double fwdSpread =
        INDEX_CAL.defaultAdjustedForwardSpread(
            fwdStartingCDX, timeToExpiry, YIELD_CURVE, adjCurves);
    final double fwdAnnuity = INDEX_CAL.indexAnnuity(fwdStartingCDX, YIELD_CURVE, adjCurves);

    final BlackIndexOptionPricer pricerWithFwd =
        new BlackIndexOptionPricer(
            fwdCDX, timeToExpiry, YIELD_CURVE, INDEX_COUPON, fwdSpread, fwdAnnuity);
    final double modStrikeLimit = INDEX_COUPON + fwdCDX.getLGD() / fwdAnnuity;
    final double vol = 0.4;
    final double payLargeSpLimit =
        fwdAnnuity
            * BlackFormulaRepository.price(fwdSpread, modStrikeLimit, timeToExpiry, vol, true);
    final double recLargeSpLimit =
        fwdAnnuity
            * BlackFormulaRepository.price(fwdSpread, modStrikeLimit, timeToExpiry, vol, false);

    final double largeStrikeSp = 75.0;
    final double payLargeStrikeSp =
        pricerWithFwd.getOptionPriceForSpreadQuotedIndex(largeStrikeSp, vol, true);
    final double recLargeStrikeSp =
        pricerWithFwd.getOptionPriceForSpreadQuotedIndex(largeStrikeSp, vol, false);
    assertEquals(payLargeSpLimit, payLargeStrikeSp, 1.e-12);
    assertEquals(
        recLargeSpLimit,
        recLargeStrikeSp,
        1.e-3); // larger strike spread ends up with failure in root-finding

    /** Exception expected */
    final double negativeTime = -0.5;
    try {
      new BlackIndexOptionPricer(
          fwdCDX, negativeTime, YIELD_CURVE, INDEX_COUPON, fwdSpread, fwdAnnuity);
      throw new RuntimeException();
    } catch (final Exception e) {
      assertEquals("timeToExpiry must be positive. Value given " + negativeTime, e.getMessage());
    }
    try {
      new BlackIndexOptionPricer(
          fwdStartingCDX, timeToExpiry, YIELD_CURVE, INDEX_COUPON, fwdSpread, fwdAnnuity);
      throw new RuntimeException();
    } catch (final Exception e) {
      assertEquals("fwdCDS should be a Forward CDS", e.getMessage());
    }
    final double negativeCoupon = -150.0 * 1.0e-4;
    try {
      new BlackIndexOptionPricer(
          fwdCDX, timeToExpiry, YIELD_CURVE, negativeCoupon, fwdSpread, fwdAnnuity);
      throw new RuntimeException();
    } catch (final Exception e) {
      assertEquals("indexCoupon must be positive", e.getMessage());
    }
    final double negativeFwdSp = -0.5;
    try {
      new BlackIndexOptionPricer(
          fwdCDX, timeToExpiry, YIELD_CURVE, INDEX_COUPON, negativeFwdSp, fwdAnnuity);
      throw new RuntimeException();
    } catch (final Exception e) {
      assertEquals("defaultAdjustedFwdSpread must be positive", e.getMessage());
    }
    final double negativeAnn = -0.3;
    try {
      new BlackIndexOptionPricer(
          fwdCDX, timeToExpiry, YIELD_CURVE, INDEX_COUPON, fwdSpread, negativeAnn);
      throw new RuntimeException();
    } catch (final Exception e) {
      assertEquals("pvFwdAnnuity must be positive", e.getMessage());
    }
    final double largeAnn = fwdCDX.getProtectionEnd() * 2.0;
    try {
      new BlackIndexOptionPricer(
          fwdCDX, timeToExpiry, YIELD_CURVE, INDEX_COUPON, fwdSpread, largeAnn);
      throw new RuntimeException();
    } catch (final Exception e) {
      assertEquals(
          "Value of annuity of "
              + largeAnn
              + " is greater than length (in years) of forward CDS. Annuity should be given for unit notional",
          e.getMessage());
    }

    final double smallStrike = -0.9;
    try {
      pricerWithFwd.getOptionPriceForPriceQuotedIndex(smallStrike, vol, true);
      throw new RuntimeException();
    } catch (final Exception e) {
      assertTrue(e instanceof IllegalArgumentException);
    }
    final double largeStrike = 0.9;
    try {
      pricerWithFwd.getOptionPriceForPriceQuotedIndex(largeStrike, vol, true);
      throw new RuntimeException();
    } catch (final Exception e) {
      assertTrue(e instanceof IllegalArgumentException);
    }
    try {
      pricerWithFwd.getOptionPriceForSpreadQuotedIndex(smallStrike, vol, true);
      throw new RuntimeException();
    } catch (final Exception e) {
      assertTrue(e instanceof IllegalArgumentException);
    }
  }