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); } }