/**
   * Calculates the Black-Scholes option value of a call, i.e., the payoff max(S(T)-K,0) P, where S
   * follows a log-normal process with constant log-volatility.
   *
   * <p>The method also handles cases where the forward and/or option strike is negative and some
   * limit cases where the forward and/or the option strike is zero.
   *
   * @param forward The forward of the underlying.
   * @param volatility The Black-Scholes volatility.
   * @param optionMaturity The option maturity T.
   * @param optionStrike The option strike. If the option strike is &le; 0.0 the method returns the
   *     value of the forward contract paying S(T)-K in T.
   * @param payoffUnit The payoff unit (e.g., the discount factor)
   * @return Returns the value of a European call option under the Black-Scholes model.
   */
  public static double blackScholesGeneralizedOptionValue(
      double forward,
      double volatility,
      double optionMaturity,
      double optionStrike,
      double payoffUnit) {
    if (optionMaturity < 0) {
      return 0;
    } else if (forward < 0) {
      // We use max(X,0) = X + max(-X,0)
      return (forward - optionStrike) * payoffUnit
          + blackScholesGeneralizedOptionValue(
              -forward, volatility, optionMaturity, -optionStrike, payoffUnit);
    } else if ((forward == 0)
        || (optionStrike <= 0.0)
        || (volatility <= 0.0)
        || (optionMaturity <= 0.0)) {
      // Limit case (where dPlus = +/- infty)
      return Math.max(forward - optionStrike, 0) * payoffUnit;
    } else {
      // Calculate analytic value
      double dPlus =
          (Math.log(forward / optionStrike) + 0.5 * volatility * volatility * optionMaturity)
              / (volatility * Math.sqrt(optionMaturity));
      double dMinus = dPlus - volatility * Math.sqrt(optionMaturity);

      double valueAnalytic =
          (forward * NormalDistribution.cumulativeDistribution(dPlus)
                  - optionStrike * NormalDistribution.cumulativeDistribution(dMinus))
              * payoffUnit;

      return valueAnalytic;
    }
  }
  /**
   * This static method calculated the rho of a call option under a Black-Scholes model
   *
   * @param initialStockValue The initial value of the underlying, i.e., the spot.
   * @param riskFreeRate The risk free rate of the bank account numerarie.
   * @param volatility The Black-Scholes volatility.
   * @param optionMaturity The option maturity T.
   * @param optionStrike The option strike.
   * @return The rho of the option
   */
  public static double blackScholesOptionRho(
      double initialStockValue,
      double riskFreeRate,
      double volatility,
      double optionMaturity,
      double optionStrike) {
    if (optionStrike <= 0.0 || optionMaturity <= 0.0) {
      // The Black-Scholes model does not consider it being an option
      return 0.0;
    } else {
      // Calculate delta
      double dMinus =
          (Math.log(initialStockValue / optionStrike)
                  + (riskFreeRate - 0.5 * volatility * volatility) * optionMaturity)
              / (volatility * Math.sqrt(optionMaturity));

      double rho =
          optionStrike
              * optionMaturity
              * Math.exp(-riskFreeRate * optionMaturity)
              * NormalDistribution.cumulativeDistribution(dMinus);

      return rho;
    }
  }
  /**
   * Calculates the Black-Scholes option value of an atm call option.
   *
   * @param volatility The Black-Scholes volatility.
   * @param optionMaturity The option maturity T.
   * @param forward The forward, i.e., the expectation of the index under the measure associated
   *     with payoff unit.
   * @param payoffUnit The payoff unit, i.e., the discount factor or the anuity associated with the
   *     payoff.
   * @return Returns the value of a European at-the-money call option under the Black-Scholes model
   */
  public static double blackScholesATMOptionValue(
      double volatility, double optionMaturity, double forward, double payoffUnit) {
    if (optionMaturity < 0) return 0.0;

    // Calculate analytic value
    double dPlus = 0.5 * volatility * Math.sqrt(optionMaturity);
    double dMinus = -dPlus;

    double valueAnalytic =
        (NormalDistribution.cumulativeDistribution(dPlus)
                - NormalDistribution.cumulativeDistribution(dMinus))
            * forward
            * payoffUnit;

    return valueAnalytic;
  }
  /**
   * Calculates the option value of a call, i.e., the payoff max(S(T)-K,0) P, where S follows a
   * normal process with constant volatility, i.e., a Bachelier model.
   *
   * @param forward The forward of the underlying.
   * @param volatility The Bachelier volatility.
   * @param optionMaturity The option maturity T.
   * @param optionStrike The option strike.
   * @param payoffUnit The payoff unit (e.g., the discount factor)
   * @return Returns the value of a European call option under the Bachelier model.
   */
  public static double bachelierOptionValue(
      double forward,
      double volatility,
      double optionMaturity,
      double optionStrike,
      double payoffUnit) {
    if (optionMaturity < 0) {
      return 0;
    } else {
      // Calculate analytic value
      double dPlus = (forward - optionStrike) / (volatility * Math.sqrt(optionMaturity));

      double valueAnalytic =
          ((forward - optionStrike) * NormalDistribution.cumulativeDistribution(dPlus)
                  + (volatility * Math.sqrt(optionMaturity)) * NormalDistribution.density(dPlus))
              * payoffUnit;

      return valueAnalytic;
    }
  }
 public static org.nd4j.linalg.api.rng.distribution.Distribution createDistribution(
     Distribution dist) {
   if (dist == null) return null;
   if (dist instanceof NormalDistribution) {
     NormalDistribution nd = (NormalDistribution) dist;
     return Nd4j.getDistributions().createNormal(nd.getMean(), nd.getStd());
   }
   if (dist instanceof GaussianDistribution) {
     GaussianDistribution nd = (GaussianDistribution) dist;
     return Nd4j.getDistributions().createNormal(nd.getMean(), nd.getStd());
   }
   if (dist instanceof UniformDistribution) {
     UniformDistribution ud = (UniformDistribution) dist;
     return Nd4j.getDistributions().createUniform(ud.getLower(), ud.getUpper());
   }
   if (dist instanceof BinomialDistribution) {
     BinomialDistribution bd = (BinomialDistribution) dist;
     return Nd4j.getDistributions()
         .createBinomial(bd.getNumberOfTrials(), bd.getProbabilityOfSuccess());
   }
   throw new RuntimeException("unknown distribution type: " + dist.getClass());
 }
  /**
   * Calculates the Bachelier option implied volatility of a call, i.e., the payoff
   *
   * <p><i>max(S(T)-K,0)</i>, where <i>S</i> follows a normal process with constant volatility.
   *
   * @param forward The forward of the underlying.
   * @param optionMaturity The option maturity T.
   * @param optionStrike The option strike. If the option strike is &le; 0.0 the method returns the
   *     value of the forward contract paying S(T)-K in T.
   * @param payoffUnit The payoff unit (e.g., the discount factor)
   * @param optionValue The option value.
   * @return Returns the implied volatility of a European call option under the Bachelier model.
   */
  public static double bachelierOptionImpliedVolatility(
      double forward,
      double optionMaturity,
      double optionStrike,
      double payoffUnit,
      double optionValue) {
    // Limit the maximum number of iterations, to ensure this calculation returns fast, e.g. in
    // cases when there is no such thing as an implied vol
    // TODO: An exception should be thrown, when there is no implied volatility for the given value.
    int maxIterations = 100;
    double maxAccuracy = 0.0;

    if (optionStrike <= 0.0) {
      // Actually it is not an option
      return 0.0;
    } else {
      // Calculate an lower and upper bound for the volatility
      double volatilityLowerBound = 0.0;
      double volatilityUpperBound =
          (optionValue + Math.abs(forward - optionStrike)) / Math.sqrt(optionMaturity) / payoffUnit;
      volatilityUpperBound /=
          Math.min(
              1.0,
              NormalDistribution.density(
                  (forward - optionStrike) / (volatilityUpperBound * Math.sqrt(optionMaturity))));

      // Solve for implied volatility
      GoldenSectionSearch solver =
          new GoldenSectionSearch(volatilityLowerBound, volatilityUpperBound);
      while (solver.getAccuracy() > maxAccuracy
          && !solver.isDone()
          && solver.getNumberOfIterations() < maxIterations) {
        double volatility = solver.getNextPoint();

        double valueAnalytic =
            bachelierOptionValue(forward, volatility, optionMaturity, optionStrike, payoffUnit);

        double error = valueAnalytic - optionValue;

        solver.setValue(error * error);
      }

      return solver.getBestPoint();
    }
  }
  /**
   * Calculates the Black-Scholes option value of a digital call option.
   *
   * @param initialStockValue The initial value of the underlying, i.e., the spot.
   * @param riskFreeRate The risk free rate of the bank account numerarie.
   * @param volatility The Black-Scholes volatility.
   * @param optionMaturity The option maturity T.
   * @param optionStrike The option strike.
   * @return Returns the value of a European call option under the Black-Scholes model
   */
  public static double blackScholesDigitalOptionValue(
      double initialStockValue,
      double riskFreeRate,
      double volatility,
      double optionMaturity,
      double optionStrike) {
    if (optionStrike <= 0.0) {
      // The Black-Scholes model does not consider it being an option
      return 1.0;
    } else {
      // Calculate analytic value
      double dPlus =
          (Math.log(initialStockValue / optionStrike)
                  + (riskFreeRate + 0.5 * volatility * volatility) * optionMaturity)
              / (volatility * Math.sqrt(optionMaturity));
      double dMinus = dPlus - volatility * Math.sqrt(optionMaturity);

      double valueAnalytic =
          Math.exp(-riskFreeRate * optionMaturity)
              * NormalDistribution.cumulativeDistribution(dMinus);

      return valueAnalytic;
    }
  }
  /**
   * Calculates the delta of a call option under a Black-Scholes model
   *
   * <p>The method also handles cases where the forward and/or option strike is negative and some
   * limit cases where the forward or the option strike is zero. In the case forward = option strike
   * = 0 the method returns 1.0.
   *
   * @param initialStockValue The initial value of the underlying, i.e., the spot.
   * @param riskFreeRate The risk free rate of the bank account numerarie.
   * @param volatility The Black-Scholes volatility.
   * @param optionMaturity The option maturity T.
   * @param optionStrike The option strike.
   * @return The delta of the option
   */
  public static double blackScholesOptionDelta(
      double initialStockValue,
      double riskFreeRate,
      double volatility,
      double optionMaturity,
      double optionStrike) {
    if (optionMaturity < 0) {
      return 0;
    } else if (initialStockValue < 0) {
      // We use Indicator(S>K) = 1 - Indicator(-S>-K)
      return 1
          - blackScholesOptionDelta(
              -initialStockValue, riskFreeRate, volatility, optionMaturity, -optionStrike);
    } else if (initialStockValue == 0) {
      // Limit case (where dPlus = +/- infty)
      if (optionStrike < 0) return 1.0; // dPlus = +infinity
      else if (optionStrike > 0) return 0.0; // dPlus = -infinity
      else return 1.0; // Matter of definition of continuity of the payoff function
    } else if ((optionStrike <= 0.0)
        || (volatility <= 0.0)
        || (optionMaturity <= 0.0)) // (and initialStockValue > 0)
    {
      // The Black-Scholes model does not consider it being an option
      return 1.0;
    } else {
      // Calculate delta
      double dPlus =
          (Math.log(initialStockValue / optionStrike)
                  + (riskFreeRate + 0.5 * volatility * volatility) * optionMaturity)
              / (volatility * Math.sqrt(optionMaturity));

      double delta = NormalDistribution.cumulativeDistribution(dPlus);

      return delta;
    }
  }
  /**
   * Calculates the Black-Scholes option implied volatility of a call, i.e., the payoff
   *
   * <p><i>max(S(T)-K,0)</i>, where <i>S</i> follows a log-normal process with constant
   * log-volatility.
   *
   * @param forward The forward of the underlying.
   * @param optionMaturity The option maturity T.
   * @param optionStrike The option strike. If the option strike is &le; 0.0 the method returns the
   *     value of the forward contract paying S(T)-K in T.
   * @param payoffUnit The payoff unit (e.g., the discount factor)
   * @param optionValue The option value.
   * @return Returns the implied volatility of a European call option under the Black-Scholes model.
   */
  public static double blackScholesOptionImpliedVolatility(
      double forward,
      double optionMaturity,
      double optionStrike,
      double payoffUnit,
      double optionValue) {
    // Limit the maximum number of iterations, to ensure this calculation returns fast, e.g. in
    // cases when there is no such thing as an implied vol
    // TODO: An exception should be thrown, when there is no implied volatility for the given value.
    int maxIterations = 500;
    double maxAccuracy = 1E-15;

    if (optionStrike <= 0.0) {
      // Actually it is not an option
      return 0.0;
    } else {
      // Calculate an lower and upper bound for the volatility
      double p =
          NormalDistribution.inverseCumulativeDistribution(
                  (optionValue / payoffUnit + optionStrike) / (forward + optionStrike))
              / Math.sqrt(optionMaturity);
      double q = 2.0 * Math.abs(Math.log(forward / optionStrike)) / optionMaturity;

      double volatilityLowerBound = p + Math.sqrt(Math.max(p * p - q, 0.0));
      double volatilityUpperBound = p + Math.sqrt(p * p + q);

      // If strike is close to forward the two bounds are close to the analytic solution
      if (Math.abs(volatilityLowerBound - volatilityUpperBound) < maxAccuracy)
        return (volatilityLowerBound + volatilityUpperBound) / 2.0;

      // Solve for implied volatility
      NewtonsMethod solver =
          new NewtonsMethod(0.5 * (volatilityLowerBound + volatilityUpperBound) /* guess */);
      while (solver.getAccuracy() > maxAccuracy
          && !solver.isDone()
          && solver.getNumberOfIterations() < maxIterations) {
        double volatility = solver.getNextPoint();

        // Calculate analytic value
        double dPlus =
            (Math.log(forward / optionStrike) + 0.5 * volatility * volatility * optionMaturity)
                / (volatility * Math.sqrt(optionMaturity));
        double dMinus = dPlus - volatility * Math.sqrt(optionMaturity);
        double valueAnalytic =
            (forward * NormalDistribution.cumulativeDistribution(dPlus)
                    - optionStrike * NormalDistribution.cumulativeDistribution(dMinus))
                * payoffUnit;
        double derivativeAnalytic =
            forward
                * Math.sqrt(optionMaturity)
                * Math.exp(-0.5 * dPlus * dPlus)
                / Math.sqrt(2.0 * Math.PI)
                * payoffUnit;

        double error = valueAnalytic - optionValue;

        solver.setValueAndDerivative(error, derivativeAnalytic);
      }

      return solver.getBestPoint();
    }
  }