@Test
  public void smoothConvergenceTest() {
    final LatticeSpecification lattice = new FlexibleLatticeSpecification();

    final boolean[] tfSet = new boolean[] {true, false};
    for (final boolean isCall : tfSet) {
      for (final double strike : STRIKES) {
        for (final double interest : INTERESTS) {
          for (final double vol : VOLS) {
            for (final double dividend : DIVIDENDS) {
              double prev = SPOT;
              double diff = 0.;
              if (interest - dividend > 0.) {
                for (int i = 0; i < 15; ++i) {
                  final int nSteps = 10 + 50 * i;
                  final OptionFunctionProvider1D function =
                      new EuropeanVanillaOptionFunctionProvider(strike, TIME, nSteps, isCall);
                  final double exactDiv =
                      BlackScholesFormulaRepository.price(
                          SPOT, strike, TIME, vol, interest, interest - dividend, isCall);
                  final double resDiv =
                      _model.getPrice(lattice, function, SPOT, vol, interest, dividend);
                  diff = Math.abs(exactDiv - resDiv);
                  assertTrue(diff < prev);
                  prev = diff;
                }
              }
            }
          }
        }
      }
    }
  }
  @Test
  public void ImpliedTreeEuropeanRecoveryTest() {
    final double interest = 0.06;
    final YieldAndDiscountCurve yCrv = YieldCurve.from(ConstantDoublesCurve.from(interest));
    final double cost = 0.02;
    final double atmVol = 0.47;
    final ZonedDateTime date = DateUtils.getUTCDate(2010, 7, 1);
    final double spot = 100;
    final Function<Double, Double> smile =
        new Function<Double, Double>() {

          @Override
          public Double evaluate(final Double... tk) {
            ArgChecker.isTrue(tk.length == 2);
            final double k = tk[1];
            return atmVol + (spot - k) * 0.0005;
          }
        };

    final StandardOptionDataBundle data =
        new StandardOptionDataBundle(
            yCrv, cost, new VolatilitySurface(FunctionalDoublesSurface.from(smile)), spot, date);

    final double[] strikes = new double[] {spot * 0.9, spot, spot * 1.11};
    final int nSteps = 7;
    final double time = 1.;

    for (int i = 0; i < strikes.length; ++i) {
      final double strike = strikes[i];
      final boolean isCall = strike >= spot ? true : false;
      final OptionFunctionProvider1D function =
          new EuropeanVanillaOptionFunctionProvider(strike, time, nSteps, isCall);
      final double tree = _modelTrinomial.getPrice(function, data);
      final double black =
          BlackScholesFormulaRepository.price(
              spot, strike * 0.9, time, data.getVolatility(time, strike), interest, cost, isCall);
      assertEquals(tree, black, black * 0.2);
    }

    try {
      _model.getPrice(
          new EuropeanVanillaOptionFunctionProvider(strikes[2], time, nSteps, true), data);
      throw new RuntimeException();
    } catch (Exception e) {
      assertTrue(e instanceof IllegalArgumentException);
    }
  }
  @Test
  public void priceLatticeTest() {
    final LatticeSpecification[] lattices =
        new LatticeSpecification[] {
          new CoxRossRubinsteinLatticeSpecification(),
          new JarrowRuddLatticeSpecification(),
          new TrigeorgisLatticeSpecification(),
          new JabbourKraminYoungLatticeSpecification(),
          new TianLatticeSpecification(),
          new LeisenReimerLatticeSpecification()
        };

    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[] {31, 115, 301};
              for (final int nSteps : choicesSteps) {
                for (final double dividend : DIVIDENDS) {
                  final OptionFunctionProvider1D function =
                      new EuropeanVanillaOptionFunctionProvider(strike, TIME, nSteps, isCall);
                  final double exactDiv =
                      BlackScholesFormulaRepository.price(
                          SPOT, strike, TIME, vol, interest, interest - dividend, isCall);
                  final double resDiv =
                      _model.getPrice(lattice, function, SPOT, vol, interest, dividend);
                  final double refDiv =
                      (lattice instanceof LeisenReimerLatticeSpecification)
                          ? Math.max(exactDiv, 1.) / nSteps / nSteps
                          : Math.max(exactDiv, 1.) / Math.sqrt(nSteps);
                  assertEquals(resDiv, exactDiv, refDiv);
                }
              }
            }
          }
        }
      }
    }
  }
  /** non-constant volatility and interest rate */
  @Test
  public void timeVaryingVolTest() {
    final LatticeSpecification lattice1 = new TimeVaryingLatticeSpecification();
    final double[] time_set = new double[] {0.5, 1.2};
    final int steps = 701;
    final double[] vol = new double[steps];
    final double[] rate = new double[steps];
    final double[] dividend = new double[steps];
    final int stepsTri = 661;
    final double[] volTri = new double[stepsTri];
    final double[] rateTri = new double[stepsTri];
    final double[] dividendTri = new double[stepsTri];
    final double constA = 0.01;
    final double constB = 0.001;
    final double constC = 0.1;
    final double constD = 0.05;

    final boolean[] tfSet = new boolean[] {true, false};
    for (final boolean isCall : tfSet) {
      for (final double strike : STRIKES) {
        for (final double time : time_set) {
          for (int i = 0; i < steps; ++i) {
            rate[i] = constA + constB * i * time / steps;
            vol[i] = constC + constD * Math.sin(i * time / steps);
            dividend[i] = 0.005;
          }
          for (int i = 0; i < stepsTri; ++i) {
            rateTri[i] = constA + constB * i * time / steps;
            volTri[i] = constC + constD * Math.sin(i * time / steps);
            dividendTri[i] = 0.005;
          }
          final double rateRef = constA + 0.5 * constB * time;
          final double volRef =
              Math.sqrt(
                  constC * constC
                      + 0.5 * constD * constD
                      + 2. * constC * constD / time * (1. - Math.cos(time))
                      - constD * constD * 0.25 / time * Math.sin(2. * time));

          final OptionFunctionProvider1D functionVanilla =
              new EuropeanVanillaOptionFunctionProvider(strike, time, steps, isCall);
          final double resPrice = _model.getPrice(functionVanilla, SPOT, vol, rate, dividend);
          final GreekResultCollection resGreeks =
              _model.getGreeks(functionVanilla, SPOT, vol, rate, dividend);

          final double resPriceConst =
              _model.getPrice(lattice1, functionVanilla, SPOT, volRef, rateRef, dividend[0]);
          final GreekResultCollection resGreeksConst =
              _model.getGreeks(lattice1, functionVanilla, SPOT, volRef, rateRef, dividend[0]);
          assertEquals(resPrice, resPriceConst, Math.max(Math.abs(resPriceConst), .1) * 1.e-1);
          assertEquals(
              resGreeks.get(Greek.FAIR_PRICE),
              resGreeksConst.get(Greek.FAIR_PRICE),
              Math.max(Math.abs(resGreeksConst.get(Greek.FAIR_PRICE)), 0.1) * 0.1);
          assertEquals(
              resGreeks.get(Greek.DELTA),
              resGreeksConst.get(Greek.DELTA),
              Math.max(Math.abs(resGreeksConst.get(Greek.DELTA)), 0.1) * 0.1);
          assertEquals(
              resGreeks.get(Greek.GAMMA),
              resGreeksConst.get(Greek.GAMMA),
              Math.max(Math.abs(resGreeksConst.get(Greek.GAMMA)), 0.1) * 0.1);
          assertEquals(
              resGreeks.get(Greek.THETA),
              resGreeksConst.get(Greek.THETA),
              Math.max(Math.abs(resGreeksConst.get(Greek.THETA)), 0.1));

          final OptionFunctionProvider1D functionTri =
              new EuropeanVanillaOptionFunctionProvider(strike, time, stepsTri, isCall);
          final double resPriceTrinomial =
              _modelTrinomial.getPrice(functionTri, SPOT, volTri, rateTri, dividendTri);
          assertEquals(
              resPriceTrinomial, resPriceConst, Math.max(Math.abs(resPriceConst), .1) * 1.e-1);
          final GreekResultCollection resGreeksTrinomial =
              _modelTrinomial.getGreeks(functionTri, SPOT, volTri, rateTri, dividendTri);
          assertEquals(
              resGreeksTrinomial.get(Greek.FAIR_PRICE),
              resGreeksConst.get(Greek.FAIR_PRICE),
              Math.max(Math.abs(resGreeksConst.get(Greek.FAIR_PRICE)), 0.1) * 0.1);
          assertEquals(
              resGreeksTrinomial.get(Greek.DELTA),
              resGreeksConst.get(Greek.DELTA),
              Math.max(Math.abs(resGreeksConst.get(Greek.DELTA)), 0.1) * 0.1);
          assertEquals(
              resGreeksTrinomial.get(Greek.GAMMA),
              resGreeksConst.get(Greek.GAMMA),
              Math.max(Math.abs(resGreeksConst.get(Greek.GAMMA)), 0.1) * 0.1);
          assertEquals(
              resGreeksTrinomial.get(Greek.THETA),
              resGreeksConst.get(Greek.THETA),
              Math.max(Math.abs(resGreeksConst.get(Greek.THETA)), 0.1));
        }
      }
    }
  }
  /** 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));
                }
              }
            }
          }
        }
      }
    }
  }