diff --git a/src/main/java/net/finmath/equities/marketdata/AffineDividendStream.java b/src/main/java/net/finmath/equities/marketdata/AffineDividendStream.java index 8b69944ca5..d265a4079a 100644 --- a/src/main/java/net/finmath/equities/marketdata/AffineDividendStream.java +++ b/src/main/java/net/finmath/equities/marketdata/AffineDividendStream.java @@ -6,7 +6,6 @@ import java.util.Comparator; import java.util.HashMap; - /** * Class to store and handle a stream of affine dividends * @@ -14,108 +13,84 @@ */ public class AffineDividendStream { - private final AffineDividend[] dividendStream; - public AffineDividendStream( - final AffineDividend[] dividendStream) - { - final var diviList = Arrays.asList(dividendStream); - diviList.sort(Comparator.comparing(pt -> pt.getDate())); - this.dividendStream = diviList.toArray(new AffineDividend[0]); - } + private final AffineDividend[] dividendStream; + + public AffineDividendStream(final AffineDividend[] dividendStream) { + final var diviList = Arrays.asList(dividendStream); + diviList.sort(Comparator.comparing(pt -> pt.getDate())); + this.dividendStream = diviList.toArray(new AffineDividend[0]); + } - public ArrayList getDividendDates() - { - final var dates = new ArrayList(); - for (final AffineDividend divi : dividendStream) { - dates.add(divi.getDate()); - } - return dates; - } + public ArrayList getDividendDates() { + final var dates = new ArrayList(); + for (final AffineDividend divi : dividendStream) { + dates.add(divi.getDate()); + } + return dates; + } - public double getDividend( - final LocalDate date, - final double stockPrice) - { - for (final AffineDividend divi : dividendStream) - { - if (divi.getDate() == date) { - return divi.getDividend(stockPrice); - } - } - return 0.0; - } + public double getDividend(final LocalDate date, final double stockPrice) { + for (final AffineDividend divi : dividendStream) { + if (divi.getDate() == date) { + return divi.getDividend(stockPrice); + } + } + return 0.0; + } - public double getProportionalDividendFactor( - final LocalDate date) - { - for (final AffineDividend divi : dividendStream) - { - if (divi.getDate() == date) { - return divi.getProportionalDividendFactor(); - } - } - return 1.0; - } + public double getProportionalDividendFactor(final LocalDate date) { + for (final AffineDividend divi : dividendStream) { + if (divi.getDate() == date) { + return divi.getProportionalDividendFactor(); + } + } + return 1.0; + } - public double getCashDividend( - final LocalDate date) - { - for (final AffineDividend divi : dividendStream) - { - if (divi.getDate() == date) { - return divi.getCashDividend(); - } - } - return 0.0; - } + public double getCashDividend(final LocalDate date) { + for (final AffineDividend divi : dividendStream) { + if (divi.getDate() == date) { + return divi.getCashDividend(); + } + } + return 0.0; + } - public static AffineDividendStream getAffineDividendsFromCashDividends( - AffineDividendStream cashDividends, - HashMap transformationFactors, - LocalDate valDate, - double spot, - FlatYieldCurve repoCurve) - { - // This method takes a stream of cash dividends and converts them to affine dividends, - // by transforming a part of each cash dividend to a proportional dividend. - // The percentage of each cash dividend to be transformed to a proportional dividend - // is specified in the member propDividendFactor of the dividend. - // The transformation is done in an arbitrage-free way, i.e. the forward structure is preserved. - // This method is usefull in practice, where traders use dividend futures as input, and transform - // a part to a proportional dividend (the further away the dividend, the higher the proportional part - // and the lower the cash part. + public static AffineDividendStream getAffineDividendsFromCashDividends(AffineDividendStream cashDividends, + HashMap transformationFactors, LocalDate valDate, double spot, YieldCurve repoCurve) { + // This method takes a stream of cash dividends and converts them to affine dividends, + // by transforming a part of each cash dividend to a proportional dividend. + // The percentage of each cash dividend to be transformed to a proportional dividend + // is specified in the member propDividendFactor of the dividend. + // The transformation is done in an arbitrage-free way, i.e. the forward structure is preserved. + // This method is usefull in practice, where traders use dividend futures as input, and transform + // a part to a proportional dividend (the further away the dividend, the higher the proportional part + // and the lower the cash part. - final var dates = cashDividends.getDividendDates(); + final var dates = cashDividends.getDividendDates(); - final var affineDividends = new ArrayList(); + final var affineDividends = new ArrayList(); - for (final var date : dates) - { - if (date.isBefore(valDate)) { - continue; - } - assert cashDividends.getProportionalDividendFactor(date) == 0.0 : - "Proportional dividend different from zero for date " + date; - final var cashDividend = cashDividends.getCashDividend(date); - var fwd = spot; - for (final var otherDate : dates) - { - if (otherDate.isBefore(date) && !otherDate.isBefore(valDate)) { - fwd -= cashDividends.getCashDividend(otherDate) - * repoCurve.getForwardDiscountFactor(valDate, otherDate); - } - } - final var q = transformationFactors.get(date) * cashDividend - * repoCurve.getForwardDiscountFactor(valDate, date) - / fwd; - affineDividends.add( - new AffineDividend( - date, - (1.0 - transformationFactors.get(date)) * cashDividend, - q)); - } + for (final var date : dates) { + if (date.isBefore(valDate)) { + continue; + } + assert cashDividends.getProportionalDividendFactor( + date) == 0.0 : "Proportional dividend different from zero for date " + date; + final var cashDividend = cashDividends.getCashDividend(date); + var fwd = spot; + for (final var otherDate : dates) { + if (otherDate.isBefore(date) && !otherDate.isBefore(valDate)) { + fwd -= cashDividends.getCashDividend(otherDate) + * repoCurve.getForwardDiscountFactor(valDate, otherDate); + } + } + final var q = transformationFactors.get(date) * cashDividend + * repoCurve.getForwardDiscountFactor(valDate, date) / fwd; + affineDividends.add(new AffineDividend(date, (1.0 - transformationFactors.get(date)) * cashDividend, q)); + } - return new AffineDividendStream(affineDividends.toArray(new AffineDividend[0])); - } + return new AffineDividendStream(affineDividends.toArray(new AffineDividend[0])); + } } diff --git a/src/main/java/net/finmath/equities/marketdata/FlatYieldCurve.java b/src/main/java/net/finmath/equities/marketdata/FlatYieldCurve.java index b067b09702..18a516768f 100644 --- a/src/main/java/net/finmath/equities/marketdata/FlatYieldCurve.java +++ b/src/main/java/net/finmath/equities/marketdata/FlatYieldCurve.java @@ -1,69 +1,26 @@ package net.finmath.equities.marketdata; import java.time.LocalDate; - import net.finmath.time.daycount.DayCountConvention; /** * Class to provide methods of a flat yield curve. - * TODO This class should be integrated into or replaced by finmat-lib's curve universe. * * @author Andreas Grotz */ -public class FlatYieldCurve { - private final LocalDate curveDate; - private final double rate; - private final DayCountConvention dayCounter; - - public FlatYieldCurve( - final LocalDate curveDate, - final double rate, - final DayCountConvention dayCounter) - { - this.curveDate = curveDate; - this.rate = rate; - this.dayCounter = dayCounter; - } - - public FlatYieldCurve rollToDate(LocalDate date) - { - return new FlatYieldCurve(date, rate, dayCounter); - } - - public double getRate(double maturity) - { - assert maturity >= 0.0 : "maturity must be positive"; - return rate; - } - - public double getRate(LocalDate date) - { - return getRate(dayCounter.getDaycountFraction(curveDate, date)); - } +public class FlatYieldCurve extends YieldCurve { - public double getDiscountFactor(double maturity) - { - assert maturity >= 0.0 : "maturity must be positive"; - return Math.exp(-maturity * rate); - } + private final static int longTime = 100; - public double getForwardDiscountFactor(double start, double expiry) - { - assert start >= 0.0 : "start must be positive"; - assert expiry >= start : "start must be before expiry"; - return getDiscountFactor(expiry) / getDiscountFactor(start); - } + public FlatYieldCurve(final LocalDate curveDate, final double rate, final DayCountConvention dayCounter) { + super("NONE", curveDate, dayCounter, new LocalDate[] { curveDate.plusYears(longTime) }, new double[] { + Math.exp(-rate * dayCounter.getDaycountFraction(curveDate, curveDate.plusYears(longTime))) }); + } - public double getDiscountFactor(LocalDate date) - { - return getDiscountFactor(dayCounter.getDaycountFraction(curveDate, date)); - } + public FlatYieldCurve rollToDate(LocalDate date) { + assert date.isAfter(baseCurve.getReferenceDate()) : "can only roll to future dates"; + return new FlatYieldCurve(date, getRate(baseCurve.getReferenceDate().plusYears(longTime)), dayCounter); + } - public double getForwardDiscountFactor(LocalDate startDate, LocalDate endDate) - { - assert !startDate.isBefore(curveDate) : "start date must be after curve date"; - assert !endDate.isBefore(startDate) : "end date must be after start date"; - return getDiscountFactor(endDate) / getDiscountFactor(startDate); - } } diff --git a/src/main/java/net/finmath/equities/marketdata/YieldCurve.java b/src/main/java/net/finmath/equities/marketdata/YieldCurve.java new file mode 100644 index 0000000000..ab9b2375b4 --- /dev/null +++ b/src/main/java/net/finmath/equities/marketdata/YieldCurve.java @@ -0,0 +1,81 @@ +package net.finmath.equities.marketdata; + +import java.time.LocalDate; +import java.util.Arrays; +import net.finmath.marketdata.model.curves.CurveInterpolation.ExtrapolationMethod; +import net.finmath.marketdata.model.curves.CurveInterpolation.InterpolationEntity; +import net.finmath.marketdata.model.curves.CurveInterpolation.InterpolationMethod; +import net.finmath.marketdata.model.curves.DiscountCurveInterpolation; +import net.finmath.time.daycount.DayCountConvention; + +/** + * Class to provide methods of a yield curve. + * + * @author Andreas Grotz + */ + +public class YieldCurve { + + protected final LocalDate referenceDate; + protected final LocalDate[] discountDates; + protected final DayCountConvention dayCounter; + protected final DiscountCurveInterpolation baseCurve; + + public YieldCurve(final String name, final LocalDate referenceDate, final DayCountConvention dayCounter, + final LocalDate[] discountDates, final double[] discountFactors) { + this.dayCounter = dayCounter; + this.discountDates = discountDates; + double[] times = new double[discountDates.length]; + boolean[] isParameter = new boolean[discountDates.length]; + for (int i = 0; i < times.length; i++) { + times[i] = dayCounter.getDaycountFraction(referenceDate, discountDates[i]); + } + baseCurve = DiscountCurveInterpolation.createDiscountCurveFromDiscountFactors(name, referenceDate, times, + discountFactors, isParameter, InterpolationMethod.LINEAR, ExtrapolationMethod.CONSTANT, + InterpolationEntity.LOG_OF_VALUE_PER_TIME); + + this.referenceDate = referenceDate; + } + + public YieldCurve rollToDate(LocalDate date) { + assert date.isAfter(referenceDate) : "can only roll to future dates"; + LocalDate[] rolledDiscountDates = Arrays.stream(discountDates).filter(p -> p.isAfter(date)) + .toArray(LocalDate[]::new); + double[] rolledDiscountFactors = new double[rolledDiscountDates.length]; + for (int i = 0; i < rolledDiscountDates.length; i++) { + rolledDiscountFactors[i] = getForwardDiscountFactor(date, rolledDiscountDates[i]); + } + + return new YieldCurve(baseCurve.getName(), date, dayCounter, rolledDiscountDates, rolledDiscountFactors); + } + + public double getRate(double maturity) { + assert maturity >= 0.0 : "maturity must be positive"; + return baseCurve.getZeroRate(maturity); + } + + public double getRate(LocalDate date) { + return baseCurve.getZeroRate(dayCounter.getDaycountFraction(referenceDate, date)); + } + + public double getDiscountFactor(double maturity) { + assert maturity >= 0.0 : "maturity must be positive"; + return baseCurve.getDiscountFactor(maturity); + } + + public double getForwardDiscountFactor(double start, double expiry) { + assert start >= 0.0 : "start must be positive"; + assert expiry >= start : "start must be before expiry"; + return getDiscountFactor(expiry) / getDiscountFactor(start); + } + + public double getDiscountFactor(LocalDate date) { + return baseCurve.getDiscountFactor(dayCounter.getDaycountFraction(referenceDate, date)); + } + + public double getForwardDiscountFactor(LocalDate startDate, LocalDate endDate) { + assert !startDate.isBefore(referenceDate) : "start date must be after curve date"; + assert !endDate.isBefore(startDate) : "end date must be after start date"; + return getDiscountFactor(endDate) / getDiscountFactor(startDate); + } +} diff --git a/src/main/java/net/finmath/equities/models/BuehlerDividendForwardStructure.java b/src/main/java/net/finmath/equities/models/BuehlerDividendForwardStructure.java index d5fefe1fd0..c9b88af5a1 100644 --- a/src/main/java/net/finmath/equities/models/BuehlerDividendForwardStructure.java +++ b/src/main/java/net/finmath/equities/models/BuehlerDividendForwardStructure.java @@ -2,9 +2,8 @@ import java.time.LocalDate; import java.util.HashMap; - import net.finmath.equities.marketdata.AffineDividendStream; -import net.finmath.equities.marketdata.FlatYieldCurve; +import net.finmath.equities.marketdata.YieldCurve; import net.finmath.time.daycount.DayCountConvention; /** @@ -15,189 +14,144 @@ */ public class BuehlerDividendForwardStructure implements EquityForwardStructure { - private final LocalDate valuationDate; - private final double spot; - private final FlatYieldCurve repoCurve; - private final AffineDividendStream dividendStream; - private final DayCountConvention dayCounter; - private final HashMap dividendTimes; - - - public BuehlerDividendForwardStructure( - final LocalDate valuationDate, - final double spot, - final FlatYieldCurve repoCurve, - final AffineDividendStream dividendStream, - final DayCountConvention dayCounter) - { - this.valuationDate = valuationDate; - this.spot = spot; - this.repoCurve = repoCurve; - this.dividendStream = dividendStream; - this.dayCounter = dayCounter; - dividendTimes = new HashMap(); - for (final var date : dividendStream.getDividendDates()) - { - dividendTimes.put(date, dayCounter.getDaycountFraction(valuationDate, date)); - } - validate(); - } - - public void validate() - { - assert getFutureDividendFactor(valuationDate) <= spot : "PV of future dividends is larger than spot."; - } - - @Override - public BuehlerDividendForwardStructure cloneWithNewSpot(double newSpot) - { - return new BuehlerDividendForwardStructure( - this.valuationDate, - newSpot, - this.repoCurve, - this.dividendStream, - this.dayCounter); - } - - @Override - public BuehlerDividendForwardStructure cloneWithNewDate(LocalDate newDate) - { - return new BuehlerDividendForwardStructure( - newDate, - this.spot, - this.repoCurve.rollToDate(newDate), - this.dividendStream, - this.dayCounter); - } - - @Override - public DividendModelType getDividendModel() - { - return DividendModelType.Buehler; - } - - @Override - public LocalDate getValuationDate() - { - return valuationDate; - } - - @Override - public double getSpot() - { - return spot; - } - - @Override - public FlatYieldCurve getRepoCurve() - { - return repoCurve; - } - - @Override - public AffineDividendStream getDividendStream() - { - return dividendStream; - } - - @Override - public double getGrowthDiscountFactor(double startTime, double endTime) - { - var df = 1.0; - for (final var date : dividendStream.getDividendDates()) - { - final var dividendTime = dividendTimes.get(date); - if (dividendTime > startTime && dividendTime <= endTime) { - df *= (1.0 - dividendStream.getProportionalDividendFactor(date)); - } - } - - return df / repoCurve.getForwardDiscountFactor(startTime, endTime); - } - - @Override - public double getGrowthDiscountFactor( - LocalDate startDate, - LocalDate endDate) - { - final var startTime = dayCounter.getDaycountFraction(valuationDate, startDate); - final var endTime = dayCounter.getDaycountFraction(valuationDate, endDate); - return getGrowthDiscountFactor(startTime, endTime); - } - - @Override - public double getFutureDividendFactor(double valTime) - { - var df = 0.0; - for (final var date : dividendStream.getDividendDates()) - { - final var dividendTime = dividendTimes.get(date); - if (dividendTime > valTime) { - df += dividendStream.getCashDividend(date) / getGrowthDiscountFactor(valTime, dividendTime); - } - } - return df; - } - - @Override - public double getFutureDividendFactor(LocalDate valDate) - { - final var valTime = dayCounter.getDaycountFraction(valuationDate, valDate); - return getFutureDividendFactor(valTime); - } - - @Override - public double getForward(double expiryTime) - { - var forward = spot * getGrowthDiscountFactor(0.0, expiryTime); - for (final var date : dividendStream.getDividendDates()) - { - final var dividendTime = dividendTimes.get(date); - if (dividendTime <= expiryTime) { - forward -= dividendStream.getCashDividend(date) * getGrowthDiscountFactor(dividendTime, expiryTime); - } - } - return forward; - } - - @Override - public double getForward(LocalDate expiryDate) - { - final var expiryTime = dayCounter.getDaycountFraction(valuationDate, expiryDate); - return getForward(expiryTime); - } - - @Override - public double getDividendAdjustedStrike( - double strike, - double expiryTime) - { - return strike - getFutureDividendFactor(expiryTime); - } - - @Override - public double getDividendAdjustedStrike( - double strike, - LocalDate expiryDate) - { - return strike - getFutureDividendFactor(expiryDate); - } - - @Override - public double getLogMoneyness( - double strike, - double expiryTime) - { - return Math.log(getDividendAdjustedStrike(strike, expiryTime) - / getDividendAdjustedStrike(getForward(expiryTime), expiryTime)); - } - - @Override - public double getLogMoneyness( - double strike, - LocalDate expiryDate) - { - return Math.log(getDividendAdjustedStrike(strike, expiryDate) - / getDividendAdjustedStrike(getForward(expiryDate), expiryDate)); - } + + private final LocalDate valuationDate; + private final double spot; + private final YieldCurve repoCurve; + private final AffineDividendStream dividendStream; + private final DayCountConvention dayCounter; + private final HashMap dividendTimes; + + public BuehlerDividendForwardStructure(final LocalDate valuationDate, final double spot, final YieldCurve repoCurve, + final AffineDividendStream dividendStream, final DayCountConvention dayCounter) { + this.valuationDate = valuationDate; + this.spot = spot; + this.repoCurve = repoCurve; + this.dividendStream = dividendStream; + this.dayCounter = dayCounter; + dividendTimes = new HashMap(); + for (final var date : dividendStream.getDividendDates()) { + dividendTimes.put(date, dayCounter.getDaycountFraction(valuationDate, date)); + } + validate(); + } + + public void validate() { + assert getFutureDividendFactor(valuationDate) <= spot : "PV of future dividends is larger than spot."; + } + + @Override + public BuehlerDividendForwardStructure cloneWithNewSpot(double newSpot) { + return new BuehlerDividendForwardStructure(this.valuationDate, newSpot, this.repoCurve, this.dividendStream, + this.dayCounter); + } + + @Override + public BuehlerDividendForwardStructure cloneWithNewDate(LocalDate newDate) { + return new BuehlerDividendForwardStructure(newDate, this.spot, this.repoCurve.rollToDate(newDate), + this.dividendStream, this.dayCounter); + } + + @Override + public DividendModelType getDividendModel() { + return DividendModelType.Buehler; + } + + @Override + public LocalDate getValuationDate() { + return valuationDate; + } + + @Override + public double getSpot() { + return spot; + } + + @Override + public YieldCurve getRepoCurve() { + return repoCurve; + } + + @Override + public AffineDividendStream getDividendStream() { + return dividendStream; + } + + @Override + public double getGrowthDiscountFactor(double startTime, double endTime) { + var df = 1.0; + for (final var date : dividendStream.getDividendDates()) { + final var dividendTime = dividendTimes.get(date); + if (dividendTime > startTime && dividendTime <= endTime) { + df *= (1.0 - dividendStream.getProportionalDividendFactor(date)); + } + } + + return df / repoCurve.getForwardDiscountFactor(startTime, endTime); + } + + @Override + public double getGrowthDiscountFactor(LocalDate startDate, LocalDate endDate) { + final var startTime = dayCounter.getDaycountFraction(valuationDate, startDate); + final var endTime = dayCounter.getDaycountFraction(valuationDate, endDate); + return getGrowthDiscountFactor(startTime, endTime); + } + + @Override + public double getFutureDividendFactor(double valTime) { + var df = 0.0; + for (final var date : dividendStream.getDividendDates()) { + final var dividendTime = dividendTimes.get(date); + if (dividendTime > valTime) { + df += dividendStream.getCashDividend(date) / getGrowthDiscountFactor(valTime, dividendTime); + } + } + return df; + } + + @Override + public double getFutureDividendFactor(LocalDate valDate) { + final var valTime = dayCounter.getDaycountFraction(valuationDate, valDate); + return getFutureDividendFactor(valTime); + } + + @Override + public double getForward(double expiryTime) { + var forward = spot * getGrowthDiscountFactor(0.0, expiryTime); + for (final var date : dividendStream.getDividendDates()) { + final var dividendTime = dividendTimes.get(date); + if (dividendTime <= expiryTime) { + forward -= dividendStream.getCashDividend(date) * getGrowthDiscountFactor(dividendTime, expiryTime); + } + } + return forward; + } + + @Override + public double getForward(LocalDate expiryDate) { + final var expiryTime = dayCounter.getDaycountFraction(valuationDate, expiryDate); + return getForward(expiryTime); + } + + @Override + public double getDividendAdjustedStrike(double strike, double expiryTime) { + return strike - getFutureDividendFactor(expiryTime); + } + + @Override + public double getDividendAdjustedStrike(double strike, LocalDate expiryDate) { + return strike - getFutureDividendFactor(expiryDate); + } + + @Override + public double getLogMoneyness(double strike, double expiryTime) { + return Math.log(getDividendAdjustedStrike(strike, expiryTime) + / getDividendAdjustedStrike(getForward(expiryTime), expiryTime)); + } + + @Override + public double getLogMoneyness(double strike, LocalDate expiryDate) { + return Math.log(getDividendAdjustedStrike(strike, expiryDate) + / getDividendAdjustedStrike(getForward(expiryDate), expiryDate)); + } } diff --git a/src/main/java/net/finmath/equities/models/EquityForwardStructure.java b/src/main/java/net/finmath/equities/models/EquityForwardStructure.java index 6e0eec602a..493bba891a 100644 --- a/src/main/java/net/finmath/equities/models/EquityForwardStructure.java +++ b/src/main/java/net/finmath/equities/models/EquityForwardStructure.java @@ -6,9 +6,8 @@ package net.finmath.equities.models; import java.time.LocalDate; - import net.finmath.equities.marketdata.AffineDividendStream; -import net.finmath.equities.marketdata.FlatYieldCurve; +import net.finmath.equities.marketdata.YieldCurve; /** * I to cover the forward structure of a stock, i.e. spot, repo curve and dividends. @@ -27,45 +26,44 @@ */ public interface EquityForwardStructure { - enum DividendModelType - { - None, - Buehler, - Escrowed, - HaugHaugLewis, - } + enum DividendModelType { + None, + Buehler, + Escrowed, + HaugHaugLewis, + } - DividendModelType getDividendModel(); + DividendModelType getDividendModel(); - LocalDate getValuationDate(); + LocalDate getValuationDate(); - double getSpot(); + double getSpot(); - FlatYieldCurve getRepoCurve(); + YieldCurve getRepoCurve(); - AffineDividendStream getDividendStream(); + AffineDividendStream getDividendStream(); - EquityForwardStructure cloneWithNewSpot(double newSpot); + EquityForwardStructure cloneWithNewSpot(double newSpot); - EquityForwardStructure cloneWithNewDate(LocalDate newDate); + EquityForwardStructure cloneWithNewDate(LocalDate newDate); - double getGrowthDiscountFactor(double startTime, double endTime); + double getGrowthDiscountFactor(double startTime, double endTime); - double getGrowthDiscountFactor(LocalDate startDate, LocalDate endDate); + double getGrowthDiscountFactor(LocalDate startDate, LocalDate endDate); - double getFutureDividendFactor(double valTime); + double getFutureDividendFactor(double valTime); - double getFutureDividendFactor(LocalDate valDate); + double getFutureDividendFactor(LocalDate valDate); - double getForward(double expiryTime); + double getForward(double expiryTime); - double getForward(LocalDate expiryDate); + double getForward(LocalDate expiryDate); - double getDividendAdjustedStrike(double strike, double expiryTime); + double getDividendAdjustedStrike(double strike, double expiryTime); - double getDividendAdjustedStrike(double strike, LocalDate expiryDate); + double getDividendAdjustedStrike(double strike, LocalDate expiryDate); - double getLogMoneyness(double strike, double expiryTime); + double getLogMoneyness(double strike, double expiryTime); - double getLogMoneyness(double strike, LocalDate expiryDate); + double getLogMoneyness(double strike, LocalDate expiryDate); } diff --git a/src/main/java/net/finmath/equities/pricer/AnalyticOptionValuation.java b/src/main/java/net/finmath/equities/pricer/AnalyticOptionValuation.java index 6375ec110d..8c76ccf185 100644 --- a/src/main/java/net/finmath/equities/pricer/AnalyticOptionValuation.java +++ b/src/main/java/net/finmath/equities/pricer/AnalyticOptionValuation.java @@ -1,10 +1,8 @@ package net.finmath.equities.pricer; import java.util.HashMap; - import org.apache.commons.lang3.NotImplementedException; - -import net.finmath.equities.marketdata.FlatYieldCurve; +import net.finmath.equities.marketdata.YieldCurve; import net.finmath.equities.models.Black76Model; import net.finmath.equities.models.EquityForwardStructure; import net.finmath.equities.models.VolatilitySurface; @@ -19,193 +17,99 @@ * @author Andreas Grotz */ -public class AnalyticOptionValuation implements OptionValuation -{ - - private final DayCountConvention dcc; - - public AnalyticOptionValuation( - DayCountConvention dcc) { - this.dcc = dcc; - } - - @Override - public EquityValuationResult calculate( - EquityValuationRequest request, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volaSurface) - { - final var results = new HashMap(); - for (final var calcType : request.getCalcsRequested()) { - results.put(calcType, calculate(request.getOption(), forwardStructure, discountCurve, volaSurface, calcType)); - } - - return new EquityValuationResult(request, results); - } - - - public double calculate( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volaSurface, - CalculationRequestType calcType) - { - assert !option.isAmericanOption() : "Analytic pricer cannot handle American options."; - final var valDate = forwardStructure.getValuationDate(); - final var expiryDate = option.getExpiryDate(); - final var ttm = dcc.getDaycountFraction(forwardStructure.getValuationDate(), expiryDate); - final var forward = forwardStructure.getForward(expiryDate); - final var discountFactor = discountCurve.getDiscountFactor(expiryDate); - final var discountRate = discountCurve.getRate(expiryDate); - final var adjustedForward = forwardStructure.getDividendAdjustedStrike(forward, expiryDate); - final var adjustedStrike = forwardStructure.getDividendAdjustedStrike(option.getStrike(), expiryDate); - final var volatility = volaSurface.getVolatility( - option.getStrike(), - option.getExpiryDate(), - forwardStructure); - - switch(calcType) - { - case Price: - return Black76Model.optionPrice( - 1.0, - adjustedStrike / adjustedForward, - ttm, - volatility, - option.isCallOption(), - discountFactor * adjustedForward); - case EqDelta: - final var dFdS = forwardStructure.getGrowthDiscountFactor(valDate, expiryDate); - return dFdS * Black76Model.optionDelta( - 1.0, - adjustedStrike / adjustedForward, - ttm, - volatility, - option.isCallOption(), - discountFactor); - case EqGamma: - final var dFdS2 = Math.pow(forwardStructure.getGrowthDiscountFactor(valDate, expiryDate), 2); - return dFdS2 * Black76Model.optionGamma( - 1.0, - adjustedStrike / adjustedForward, - ttm, - volatility, - option.isCallOption(), - discountFactor / adjustedForward); - case EqVega: - return Black76Model.optionVega( - 1.0, - adjustedStrike / adjustedForward, - ttm, - volatility, - option.isCallOption(), - discountFactor * adjustedForward); - case Theta: - return Black76Model.optionTheta( - 1.0, - adjustedStrike / adjustedForward, - ttm, - volatility, - option.isCallOption(), - discountFactor * adjustedForward, - discountRate); - default: - throw new NotImplementedException("Calculation for " + calcType + " not implemented yet."); - } - } - - public double getPrice( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volaSurface) - { - return calculate( - option, - forwardStructure, - discountCurve, - volaSurface, - CalculationRequestType.Price); - } - - public double getDelta( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volaSurface) - { - return calculate( - option, - forwardStructure, - discountCurve, - volaSurface, - CalculationRequestType.EqDelta); - } - - public double getGamma( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volaSurface) - { - return calculate( - option, - forwardStructure, - discountCurve, - volaSurface, - CalculationRequestType.EqGamma); - } - - public double getVega( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volaSurface) - { - return calculate( - option, - forwardStructure, - discountCurve, - volaSurface, - CalculationRequestType.EqVega); - } - - public double getTheta( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volaSurface) - { - return calculate( - option, - forwardStructure, - discountCurve, - volaSurface, - CalculationRequestType.Theta); - } - - public double getImpliedVolatility( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - double price) - { - assert !option.isAmericanOption() : "Analytic pricer cannot handle American options."; - final var expiryDate = option.getExpiryDate(); - final var ttm = dcc.getDaycountFraction(forwardStructure.getValuationDate(), expiryDate); - final var forward = forwardStructure.getForward(expiryDate); - final var discount = discountCurve.getDiscountFactor(expiryDate); - final var adjustedForward = forwardStructure.getDividendAdjustedStrike(forward, expiryDate); - final var adjustedStrike = forwardStructure.getDividendAdjustedStrike(option.getStrike(), expiryDate); - final var undiscountedPrice = price / discount / adjustedForward; - - return Black76Model.optionImpliedVolatility( - 1.0, - adjustedStrike / adjustedForward, - ttm, - undiscountedPrice, - option.isCallOption()); - } +public class AnalyticOptionValuation implements OptionValuation { + + private final DayCountConvention dcc; + + public AnalyticOptionValuation(DayCountConvention dcc) { + this.dcc = dcc; + } + + @Override + public EquityValuationResult calculate(EquityValuationRequest request, EquityForwardStructure forwardStructure, + YieldCurve discountCurve, VolatilitySurface volaSurface) { + final var results = new HashMap(); + for (final var calcType : request.getCalcsRequested()) { + results.put(calcType, + calculate(request.getOption(), forwardStructure, discountCurve, volaSurface, calcType)); + } + + return new EquityValuationResult(request, results); + } + + public double calculate(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + VolatilitySurface volaSurface, CalculationRequestType calcType) { + assert !option.isAmericanOption() : "Analytic pricer cannot handle American options."; + final var valDate = forwardStructure.getValuationDate(); + final var expiryDate = option.getExpiryDate(); + final var ttm = dcc.getDaycountFraction(forwardStructure.getValuationDate(), expiryDate); + final var forward = forwardStructure.getForward(expiryDate); + final var discountFactor = discountCurve.getDiscountFactor(expiryDate); + final var discountRate = discountCurve.getRate(expiryDate); + final var adjustedForward = forwardStructure.getDividendAdjustedStrike(forward, expiryDate); + final var adjustedStrike = forwardStructure.getDividendAdjustedStrike(option.getStrike(), expiryDate); + final var volatility = volaSurface.getVolatility(option.getStrike(), option.getExpiryDate(), forwardStructure); + + switch (calcType) { + case Price: + return Black76Model.optionPrice(1.0, adjustedStrike / adjustedForward, ttm, volatility, + option.isCallOption(), discountFactor * adjustedForward); + case EqDelta: + final var dFdS = forwardStructure.getGrowthDiscountFactor(valDate, expiryDate); + return dFdS * Black76Model.optionDelta(1.0, adjustedStrike / adjustedForward, ttm, volatility, + option.isCallOption(), discountFactor); + case EqGamma: + final var dFdS2 = Math.pow(forwardStructure.getGrowthDiscountFactor(valDate, expiryDate), 2); + return dFdS2 * Black76Model.optionGamma(1.0, adjustedStrike / adjustedForward, ttm, volatility, + option.isCallOption(), discountFactor / adjustedForward); + case EqVega: + return Black76Model.optionVega(1.0, adjustedStrike / adjustedForward, ttm, volatility, + option.isCallOption(), discountFactor * adjustedForward); + case Theta: + return Black76Model.optionTheta(1.0, adjustedStrike / adjustedForward, ttm, volatility, + option.isCallOption(), discountFactor * adjustedForward, discountRate); + default: + throw new NotImplementedException("Calculation for " + calcType + " not implemented yet."); + } + } + + public double getPrice(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + VolatilitySurface volaSurface) { + return calculate(option, forwardStructure, discountCurve, volaSurface, CalculationRequestType.Price); + } + + public double getDelta(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + VolatilitySurface volaSurface) { + return calculate(option, forwardStructure, discountCurve, volaSurface, CalculationRequestType.EqDelta); + } + + public double getGamma(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + VolatilitySurface volaSurface) { + return calculate(option, forwardStructure, discountCurve, volaSurface, CalculationRequestType.EqGamma); + } + + public double getVega(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + VolatilitySurface volaSurface) { + return calculate(option, forwardStructure, discountCurve, volaSurface, CalculationRequestType.EqVega); + } + + public double getTheta(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + VolatilitySurface volaSurface) { + return calculate(option, forwardStructure, discountCurve, volaSurface, CalculationRequestType.Theta); + } + + public double getImpliedVolatility(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + double price) { + assert !option.isAmericanOption() : "Analytic pricer cannot handle American options."; + final var expiryDate = option.getExpiryDate(); + final var ttm = dcc.getDaycountFraction(forwardStructure.getValuationDate(), expiryDate); + final var forward = forwardStructure.getForward(expiryDate); + final var discount = discountCurve.getDiscountFactor(expiryDate); + final var adjustedForward = forwardStructure.getDividendAdjustedStrike(forward, expiryDate); + final var adjustedStrike = forwardStructure.getDividendAdjustedStrike(option.getStrike(), expiryDate); + final var undiscountedPrice = price / discount / adjustedForward; + + return Black76Model.optionImpliedVolatility(1.0, adjustedStrike / adjustedForward, ttm, undiscountedPrice, + option.isCallOption()); + } } diff --git a/src/main/java/net/finmath/equities/pricer/OptionValuation.java b/src/main/java/net/finmath/equities/pricer/OptionValuation.java index 5cf8432e69..f76a65506d 100644 --- a/src/main/java/net/finmath/equities/pricer/OptionValuation.java +++ b/src/main/java/net/finmath/equities/pricer/OptionValuation.java @@ -1,6 +1,6 @@ package net.finmath.equities.pricer; -import net.finmath.equities.marketdata.FlatYieldCurve; +import net.finmath.equities.marketdata.YieldCurve; import net.finmath.equities.models.EquityForwardStructure; import net.finmath.equities.models.VolatilitySurface; @@ -13,9 +13,6 @@ public interface OptionValuation extends Cloneable { - EquityValuationResult calculate( - EquityValuationRequest request, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volSurface); + EquityValuationResult calculate(EquityValuationRequest request, EquityForwardStructure forwardStructure, + YieldCurve discountCurve, VolatilitySurface volSurface); } diff --git a/src/main/java/net/finmath/equities/pricer/PdeOptionValuation.java b/src/main/java/net/finmath/equities/pricer/PdeOptionValuation.java index caf5833d53..e15a2a1adb 100644 --- a/src/main/java/net/finmath/equities/pricer/PdeOptionValuation.java +++ b/src/main/java/net/finmath/equities/pricer/PdeOptionValuation.java @@ -4,13 +4,11 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; - import org.apache.commons.math3.linear.DecompositionSolver; import org.apache.commons.math3.linear.LUDecomposition; import org.apache.commons.math3.linear.MatrixUtils; import org.apache.commons.math3.linear.RealMatrix; - -import net.finmath.equities.marketdata.FlatYieldCurve; +import net.finmath.equities.marketdata.YieldCurve; import net.finmath.equities.models.EquityForwardStructure; import net.finmath.equities.models.FlatVolatilitySurface; import net.finmath.equities.models.VolatilitySurface; @@ -36,434 +34,339 @@ * * @author Andreas Grotz */ -public class PdeOptionValuation implements OptionValuation -{ - - private final int timeStepsPerYear; - private final double spaceMinForwardMultiple; - private final double spaceMaxForwardMultiple; - private final int spaceNbOfSteps; - private final double spaceStepSize; - private final ArrayList spots; - private final int spotIndex; - private final DayCountConvention dayCounter; - private final boolean isLvPricer; - private final boolean includeDividendDatesInGrid; - - - public PdeOptionValuation( - double spaceMinForwardMultiple, - double spaceMaxForwardMultiple, - int spaceNbPoints, - final int timeStepsPerYear, - DayCountConvention dcc, - final boolean isLvPricer, - final boolean includeDividendDatesInGrid) - { - assert spaceMinForwardMultiple < 1.0 : "min multiple of forward must be below 1.0"; - assert spaceMaxForwardMultiple > 1.0 : "max multiple of forward must be below 1.0"; - - this.timeStepsPerYear = timeStepsPerYear; - this.dayCounter = dcc; - this.isLvPricer = isLvPricer; - this.includeDividendDatesInGrid = includeDividendDatesInGrid; - - // Set up the space grid for the pure volatility process - var tmpSpaceStepSize = (spaceMaxForwardMultiple - spaceMinForwardMultiple) / spaceNbPoints; - var tmpSpaceNbPoints = spaceNbPoints; - var tmpSpots = new ArrayList(); - for (int i = 0; i < tmpSpaceNbPoints; i++) { - tmpSpots.add(spaceMinForwardMultiple + tmpSpaceStepSize * i); - } - // The space grid needs to include the forward level 1.0 for the pure volatility process - // Hence if necessary, we increase the step size slightly to include it - final var lowerBound = Math.abs(Collections.binarySearch(tmpSpots, 1.0)) - 2; - if (!(tmpSpots.get(lowerBound) == 1.0)) - { - tmpSpaceStepSize += (1.0 - tmpSpots.get(lowerBound)) / lowerBound; - tmpSpots = new ArrayList(); - tmpSpaceNbPoints = 0; - var tmpSpot = 0.0; - while (tmpSpot < spaceMaxForwardMultiple) - { - tmpSpot = spaceMinForwardMultiple + tmpSpaceStepSize * tmpSpaceNbPoints; - tmpSpots.add(tmpSpot); - tmpSpaceNbPoints++; - } - } - - this.spaceMinForwardMultiple = spaceMinForwardMultiple; - this.spaceMaxForwardMultiple = tmpSpots.get(tmpSpots.size() - 1); - this.spaceNbOfSteps = tmpSpaceNbPoints; - spots = tmpSpots; - spaceStepSize = tmpSpaceStepSize; - spotIndex = lowerBound; - } - - @Override - public EquityValuationResult calculate( - EquityValuationRequest request, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volaSurface) - { - final var results = new HashMap(); - if(request.getCalcsRequested().isEmpty()) { - return new EquityValuationResult(request, results); - } - - double price = 0.0; - if(request.getCalcsRequested().contains(CalculationRequestType.EqDelta) - || request.getCalcsRequested().contains(CalculationRequestType.EqGamma )) - { - final var spotSensis = getPdeSensis( - request.getOption(), - forwardStructure, - discountCurve, - volaSurface); - price = spotSensis[0]; - if(request.getCalcsRequested().contains(CalculationRequestType.EqDelta)) { - results.put(CalculationRequestType.EqDelta, spotSensis[1]); - } - if(request.getCalcsRequested().contains(CalculationRequestType.EqGamma)) { - results.put(CalculationRequestType.EqGamma, spotSensis[2]); - } - } - else - { - price = getPrice( - request.getOption(), - forwardStructure, - discountCurve, - volaSurface); - } - - if(request.getCalcsRequested().contains(CalculationRequestType.Price)) { - results.put(CalculationRequestType.Price, price); - } - - if(request.getCalcsRequested().contains(CalculationRequestType.EqVega)) - { - final var volShift = 0.0001; // TODO Make part of class members - final var priceShifted = getPrice( - request.getOption(), - forwardStructure, - discountCurve, - volaSurface.getShiftedSurface(volShift)); - results.put(CalculationRequestType.EqVega, (priceShifted - price) / volShift); - } - - return new EquityValuationResult(request, results); - } - - public double getPrice( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volSurface) - { - return evolvePde(option, forwardStructure, discountCurve, volSurface, false)[0]; - } - - public double[] getPdeSensis( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volSurface) - { - return evolvePde(option, forwardStructure, discountCurve, volSurface, true); - } - - public double getVega( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volSurface, - double basePrice, - double volShift) - { - final var shiftedPrice = getPrice(option, forwardStructure, discountCurve, volSurface.getShiftedSurface(volShift)); - return (shiftedPrice - basePrice) / volShift; - } - - public double getTheta( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volSurface, - double basePrice) - { - final var valDate = forwardStructure.getValuationDate(); - final var thetaDate = valDate.plusDays(1); - final var thetaSpot = forwardStructure.getForward(thetaDate); - final var shiftedFwdStructure = forwardStructure.cloneWithNewSpot(thetaSpot).cloneWithNewDate(thetaDate); - final var shiftedPrice = getPrice(option, shiftedFwdStructure, discountCurve, volSurface); - return (shiftedPrice - basePrice) / dayCounter.getDaycountFraction(valDate, thetaDate); - } - - private double[] evolvePde( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - VolatilitySurface volSurface, - boolean calculateSensis) - { - // Get data - final var valDate = forwardStructure.getValuationDate(); - final var expiryDate = option.getExpiryDate(); - final var expiryTime = dayCounter.getDaycountFraction(valDate, expiryDate); - assert !forwardStructure.getValuationDate().isAfter(expiryDate) - : "Valuation date must not be after option expiry"; - final var impliedVol = volSurface.getVolatility(option.getStrike(), expiryDate, forwardStructure); - var forward = forwardStructure.getForward(expiryDate); - var fdf = forwardStructure.getFutureDividendFactor(expiryDate); - - // Build matrices - final RealMatrix idMatrix = MatrixUtils.createRealIdentityMatrix(spaceNbOfSteps); - final RealMatrix tridiagMatrix = MatrixUtils.createRealMatrix(spaceNbOfSteps, spaceNbOfSteps); - final double spaceStepSq = spaceStepSize * spaceStepSize; - for (int i = 0; i < spaceNbOfSteps; i++) { - for (int j = 0; j < spaceNbOfSteps; j++) { - if (i == j) - { - tridiagMatrix.setEntry(i, j, Math.pow(spots.get(i), 2) / spaceStepSq); - } - else if (i == j - 1 || i == j + 1) - { - tridiagMatrix.setEntry(i, j, -0.5 * Math.pow(spots.get(i), 2) / spaceStepSq); - } - else - { - tridiagMatrix.setEntry(i, j, 0); - } - } - } - - // Set initial values - var prices = MatrixUtils.createRealVector(new double[spaceNbOfSteps]); - for (int i = 0; i < spaceNbOfSteps; i++) - { - prices.setEntry(i, option.getPayoff((forward - fdf) * spots.get(i) + fdf)); - } - - // Set time intervals to evolve the PDE (i.e. from dividend to dividend) - final var diviDates = forwardStructure.getDividendStream().getDividendDates(); - final var anchorTimes = new ArrayList (); - anchorTimes.add(0.0); - if(includeDividendDatesInGrid) - { - for (final var date : diviDates) - { - if (date.isAfter(valDate) && date.isBefore(expiryDate)) - { - anchorTimes.add(dayCounter.getDaycountFraction(valDate, date)); - } - } - } - anchorTimes.add(expiryTime); - anchorTimes.sort(Comparator.comparing(pt -> pt)); - var lastAtmPrice = 0.0; - var dt = 0.0; - - // Evolve PDE - for (int a = anchorTimes.size() - 1; a > 0; a--) - { - // Set time steps - final var timeInterval = anchorTimes.get(a) - anchorTimes.get(a - 1); - int timeNbOfSteps; - double timeStepSize; - if (timeStepsPerYear == 0) // Use optimal ratio of time and space step size - { - timeNbOfSteps = (int)Math.ceil(2 * impliedVol * Math.pow(timeInterval, 1.5) / spaceStepSize); - timeStepSize = timeInterval / timeNbOfSteps; - } - else // Use time step size provided externally - { - timeNbOfSteps = (int)Math.floor(timeInterval * timeStepsPerYear); - timeStepSize = timeInterval / timeNbOfSteps; - } - - final var times = new ArrayList(); - for (int i = 0; i <= 4; i++) { - times.add(anchorTimes.get(a) - i * 0.25 * timeStepSize); - } - for (int i = timeNbOfSteps - 2; i >= 0; i--) { - times.add(anchorTimes.get(a - 1) + i * timeStepSize); - } - - // Evolve PDE in current time interval - for (int i = 1; i < times.size(); i++) - { - lastAtmPrice = prices.getEntry(spotIndex); - dt = times.get(i-1) - times.get(i); - double theta = 0.5; - if (i <= 4) { - theta = 1.0; - } - final var theta1 = 1.0 - theta; - final var volSq = impliedVol * impliedVol; - - RealMatrix implicitMatrix, explicitMatrix; - if (isLvPricer) - { - implicitMatrix = tridiagMatrix.scalarMultiply(theta * dt); - explicitMatrix = tridiagMatrix.scalarMultiply(-theta1 * dt); - final var localVol = new double[spaceNbOfSteps]; - for (int s = 0; s < spaceNbOfSteps; s++) - { - final var lv = volSurface.getLocalVolatility( - Math.log(spots.get(s)), times.get(i-1), forwardStructure, spaceStepSize, dt); - localVol[s] = lv * lv; - } - - final var volaMatrix = MatrixUtils.createRealDiagonalMatrix(localVol); - implicitMatrix = volaMatrix.multiply(implicitMatrix); - explicitMatrix = volaMatrix.multiply(explicitMatrix); - } - else - { - implicitMatrix = tridiagMatrix.scalarMultiply(theta * dt * volSq); - explicitMatrix = tridiagMatrix.scalarMultiply(-theta1 * dt * volSq); - } - - implicitMatrix = idMatrix.add(implicitMatrix); - explicitMatrix = idMatrix.add(explicitMatrix); - - if (option.isAmericanOption()) - { - // Use the penalty algorithm from Forsyth's 2001 paper to solve the - // linear complementary problem for the American exercise feature. - final var penaltyFactor = 1 / Math.min(timeStepSize * timeStepSize, spaceStepSize * spaceStepSize); - forward = forwardStructure.getForward(times.get(i)); - fdf = forwardStructure.getFutureDividendFactor(times.get(i)); - final var discountFactor = discountCurve.getForwardDiscountFactor(times.get(i), expiryTime); - final var payoffs = MatrixUtils.createRealVector(new double[spaceNbOfSteps]); - final var penaltyMatrix = MatrixUtils.createRealMatrix(spaceNbOfSteps, spaceNbOfSteps); - for (int j = 1; j < spaceNbOfSteps - 1; j++) - { - final var payoff = option.getPayoff((forward - fdf) * spots.get(j) + fdf) - / discountFactor; - payoffs.setEntry(j, payoff); - penaltyMatrix.setEntry(j, j, prices.getEntry(j) < payoff ? penaltyFactor : 0); - } - - final var b = explicitMatrix.operate(prices); - var oldPrices = prices.copy(); - final var oldPenaltyMatrix = penaltyMatrix.copy(); - final var tol = 1 / penaltyFactor; - int iterations = 0; - while (true) - { - assert iterations++ < 100 : "Penalty algorithm for american exercise did not converge in 100 steps"; - final var c = b.add(penaltyMatrix.operate(payoffs)); - final var A = implicitMatrix.add(penaltyMatrix); - final DecompositionSolver solver = new LUDecomposition(A).getSolver(); - prices = solver.solve(c); - for (int j = 1; j < spaceNbOfSteps - 1; j++) - { - penaltyMatrix.setEntry(j, j, prices.getEntry(j) < payoffs.getEntry(j) ? penaltyFactor : 0); - } - - if (penaltyMatrix.equals(oldPenaltyMatrix) - || (prices.subtract(oldPrices).getLInfNorm()) - / Math.max(oldPrices.getLInfNorm(), 1.0) < tol) - { - break; - } - oldPrices = prices.copy(); - } - - } - else - { - // Solve the PDE step directly - prices = explicitMatrix.operate(prices); - final DecompositionSolver solver = new LUDecomposition(implicitMatrix).getSolver(); - prices = solver.solve(prices); - } - - // Set boundary conditions - prices.setEntry(0, option.getPayoff((forward - fdf) * spaceMinForwardMultiple + fdf)); - prices.setEntry(spaceNbOfSteps - 1, option.getPayoff((forward - fdf) * spaceMaxForwardMultiple + fdf)); - } - } - - final var discountFactor = discountCurve.getDiscountFactor(expiryDate); - final var price = discountFactor * prices.getEntry(spotIndex); - - if (calculateSensis) - { - final var dFdX = forwardStructure.getDividendAdjustedStrike( - forwardStructure.getForward(expiryDate), expiryDate); - final var dFdS = forwardStructure.getGrowthDiscountFactor(valDate, expiryDate); - final var delta = discountFactor * 0.5 - * (prices.getEntry(spotIndex + 1) - prices.getEntry(spotIndex - 1)) / spaceStepSize - * dFdS / dFdX; - final var gamma = discountFactor * (prices.getEntry(spotIndex + 1) + prices.getEntry(spotIndex - 1) - - 2 * prices.getEntry(spotIndex)) / spaceStepSq * dFdS * dFdS / dFdX / dFdX; - final var discountFactorTheta = discountCurve.getDiscountFactor(expiryTime - dt); - final var theta = (discountFactorTheta * lastAtmPrice - price) / dt; - return new double[] {price, delta, gamma, theta}; - } - else - { - return new double[] {price, Double.NaN, Double.NaN, Double.NaN}; - } - } - - - public double getImpliedVolatility( - Option option, - EquityForwardStructure forwardStructure, - FlatYieldCurve discountCurve, - double price) - { - double initialGuess = 0.25; - final var forward = forwardStructure.getForward(option.getExpiryDate()); - // Use analytic pricer as initial guess for Europeans and OTM Americans - // Use two bisection steps for ITM Americans - if(option.isAmericanOption() && option.getPayoff(forward) > 0.0) - { - final var bisectionSolver = new BisectionSearch(0.00001,1.0); - for (int i = 0; i < 3; i++) - { - final double currentVol = bisectionSolver.getNextPoint(); - final double currentPrice = getPrice( - option, - forwardStructure, - discountCurve, - new FlatVolatilitySurface(currentVol)); - - bisectionSolver.setValue(currentPrice - price); - } - initialGuess = bisectionSolver.getBestPoint(); - } - else - { - final var anaPricer = new AnalyticOptionValuation(dayCounter); - Option testOption; - if(option.isAmericanOption()) { - testOption = new EuropeanOption(option.getExpiryDate(), option.getStrike(), option.isCallOption()); - } else { - testOption = option; - } - initialGuess = anaPricer.getImpliedVolatility(testOption, forwardStructure, discountCurve, price); - - } - - // Solve for implied vol - final var solver = new SecantMethod(initialGuess, initialGuess * 1.01); - while(solver.getAccuracy() / price > 1e-3 && !solver.isDone()) { - final double currentVol = solver.getNextPoint(); - final double currentPrice = getPrice( - option, - forwardStructure, - discountCurve, - new FlatVolatilitySurface(currentVol)); - - solver.setValue(currentPrice - price); - } - - return Math.abs(solver.getBestPoint()); // Note that the PDE only uses sigma^2 - } +public class PdeOptionValuation implements OptionValuation { + + private final int timeStepsPerYear; + private final double spaceMinForwardMultiple; + private final double spaceMaxForwardMultiple; + private final int spaceNbOfSteps; + private final double spaceStepSize; + private final ArrayList spots; + private final int spotIndex; + private final DayCountConvention dayCounter; + private final boolean isLvPricer; + private final boolean includeDividendDatesInGrid; + + public PdeOptionValuation(double spaceMinForwardMultiple, double spaceMaxForwardMultiple, int spaceNbPoints, + final int timeStepsPerYear, DayCountConvention dcc, final boolean isLvPricer, + final boolean includeDividendDatesInGrid) { + assert spaceMinForwardMultiple < 1.0 : "min multiple of forward must be below 1.0"; + assert spaceMaxForwardMultiple > 1.0 : "max multiple of forward must be below 1.0"; + + this.timeStepsPerYear = timeStepsPerYear; + this.dayCounter = dcc; + this.isLvPricer = isLvPricer; + this.includeDividendDatesInGrid = includeDividendDatesInGrid; + + // Set up the space grid for the pure volatility process + var tmpSpaceStepSize = (spaceMaxForwardMultiple - spaceMinForwardMultiple) / spaceNbPoints; + var tmpSpaceNbPoints = spaceNbPoints; + var tmpSpots = new ArrayList(); + for (int i = 0; i < tmpSpaceNbPoints; i++) { + tmpSpots.add(spaceMinForwardMultiple + tmpSpaceStepSize * i); + } + // The space grid needs to include the forward level 1.0 for the pure volatility process + // Hence if necessary, we increase the step size slightly to include it + final var lowerBound = Math.abs(Collections.binarySearch(tmpSpots, 1.0)) - 2; + if (!(tmpSpots.get(lowerBound) == 1.0)) { + tmpSpaceStepSize += (1.0 - tmpSpots.get(lowerBound)) / lowerBound; + tmpSpots = new ArrayList(); + tmpSpaceNbPoints = 0; + var tmpSpot = 0.0; + while (tmpSpot < spaceMaxForwardMultiple) { + tmpSpot = spaceMinForwardMultiple + tmpSpaceStepSize * tmpSpaceNbPoints; + tmpSpots.add(tmpSpot); + tmpSpaceNbPoints++; + } + } + + this.spaceMinForwardMultiple = spaceMinForwardMultiple; + this.spaceMaxForwardMultiple = tmpSpots.get(tmpSpots.size() - 1); + this.spaceNbOfSteps = tmpSpaceNbPoints; + spots = tmpSpots; + spaceStepSize = tmpSpaceStepSize; + spotIndex = lowerBound; + } + + @Override + public EquityValuationResult calculate(EquityValuationRequest request, EquityForwardStructure forwardStructure, + YieldCurve discountCurve, VolatilitySurface volaSurface) { + final var results = new HashMap(); + if (request.getCalcsRequested().isEmpty()) { + return new EquityValuationResult(request, results); + } + + double price = 0.0; + if (request.getCalcsRequested().contains(CalculationRequestType.EqDelta) + || request.getCalcsRequested().contains(CalculationRequestType.EqGamma)) { + final var spotSensis = getPdeSensis(request.getOption(), forwardStructure, discountCurve, volaSurface); + price = spotSensis[0]; + if (request.getCalcsRequested().contains(CalculationRequestType.EqDelta)) { + results.put(CalculationRequestType.EqDelta, spotSensis[1]); + } + if (request.getCalcsRequested().contains(CalculationRequestType.EqGamma)) { + results.put(CalculationRequestType.EqGamma, spotSensis[2]); + } + } else { + price = getPrice(request.getOption(), forwardStructure, discountCurve, volaSurface); + } + + if (request.getCalcsRequested().contains(CalculationRequestType.Price)) { + results.put(CalculationRequestType.Price, price); + } + + if (request.getCalcsRequested().contains(CalculationRequestType.EqVega)) { + final var volShift = 0.0001; // TODO Make part of class members + final var priceShifted = getPrice(request.getOption(), forwardStructure, discountCurve, + volaSurface.getShiftedSurface(volShift)); + results.put(CalculationRequestType.EqVega, (priceShifted - price) / volShift); + } + + return new EquityValuationResult(request, results); + } + + public double getPrice(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + VolatilitySurface volSurface) { + return evolvePde(option, forwardStructure, discountCurve, volSurface, false)[0]; + } + + public double[] getPdeSensis(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + VolatilitySurface volSurface) { + return evolvePde(option, forwardStructure, discountCurve, volSurface, true); + } + + public double getVega(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + VolatilitySurface volSurface, double basePrice, double volShift) { + final var shiftedPrice = getPrice(option, forwardStructure, discountCurve, + volSurface.getShiftedSurface(volShift)); + return (shiftedPrice - basePrice) / volShift; + } + + public double getTheta(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + VolatilitySurface volSurface, double basePrice) { + final var valDate = forwardStructure.getValuationDate(); + final var thetaDate = valDate.plusDays(1); + final var thetaSpot = forwardStructure.getForward(thetaDate); + final var shiftedFwdStructure = forwardStructure.cloneWithNewSpot(thetaSpot).cloneWithNewDate(thetaDate); + final var shiftedPrice = getPrice(option, shiftedFwdStructure, discountCurve, volSurface); + return (shiftedPrice - basePrice) / dayCounter.getDaycountFraction(valDate, thetaDate); + } + + private double[] evolvePde(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + VolatilitySurface volSurface, boolean calculateSensis) { + // Get data + final var valDate = forwardStructure.getValuationDate(); + final var expiryDate = option.getExpiryDate(); + final var expiryTime = dayCounter.getDaycountFraction(valDate, expiryDate); + assert !forwardStructure.getValuationDate() + .isAfter(expiryDate) : "Valuation date must not be after option expiry"; + final var impliedVol = volSurface.getVolatility(option.getStrike(), expiryDate, forwardStructure); + var forward = forwardStructure.getForward(expiryDate); + var fdf = forwardStructure.getFutureDividendFactor(expiryDate); + + // Build matrices + final RealMatrix idMatrix = MatrixUtils.createRealIdentityMatrix(spaceNbOfSteps); + final RealMatrix tridiagMatrix = MatrixUtils.createRealMatrix(spaceNbOfSteps, spaceNbOfSteps); + final double spaceStepSq = spaceStepSize * spaceStepSize; + for (int i = 0; i < spaceNbOfSteps; i++) { + for (int j = 0; j < spaceNbOfSteps; j++) { + if (i == j) { + tridiagMatrix.setEntry(i, j, Math.pow(spots.get(i), 2) / spaceStepSq); + } else if (i == j - 1 || i == j + 1) { + tridiagMatrix.setEntry(i, j, -0.5 * Math.pow(spots.get(i), 2) / spaceStepSq); + } else { + tridiagMatrix.setEntry(i, j, 0); + } + } + } + + // Set initial values + var prices = MatrixUtils.createRealVector(new double[spaceNbOfSteps]); + for (int i = 0; i < spaceNbOfSteps; i++) { + prices.setEntry(i, option.getPayoff((forward - fdf) * spots.get(i) + fdf)); + } + + // Set time intervals to evolve the PDE (i.e. from dividend to dividend) + final var diviDates = forwardStructure.getDividendStream().getDividendDates(); + final var anchorTimes = new ArrayList(); + anchorTimes.add(0.0); + if (includeDividendDatesInGrid) { + for (final var date : diviDates) { + if (date.isAfter(valDate) && date.isBefore(expiryDate)) { + anchorTimes.add(dayCounter.getDaycountFraction(valDate, date)); + } + } + } + anchorTimes.add(expiryTime); + anchorTimes.sort(Comparator.comparing(pt -> pt)); + var lastAtmPrice = 0.0; + var dt = 0.0; + + // Evolve PDE + for (int a = anchorTimes.size() - 1; a > 0; a--) { + // Set time steps + final var timeInterval = anchorTimes.get(a) - anchorTimes.get(a - 1); + int timeNbOfSteps; + double timeStepSize; + if (timeStepsPerYear == 0) // Use optimal ratio of time and space step size + { + timeNbOfSteps = (int) Math.ceil(2 * impliedVol * Math.pow(timeInterval, 1.5) / spaceStepSize); + timeStepSize = timeInterval / timeNbOfSteps; + } else // Use time step size provided externally + { + timeNbOfSteps = (int) Math.floor(timeInterval * timeStepsPerYear); + timeStepSize = timeInterval / timeNbOfSteps; + } + + final var times = new ArrayList(); + for (int i = 0; i <= 4; i++) { + times.add(anchorTimes.get(a) - i * 0.25 * timeStepSize); + } + for (int i = timeNbOfSteps - 2; i >= 0; i--) { + times.add(anchorTimes.get(a - 1) + i * timeStepSize); + } + + // Evolve PDE in current time interval + for (int i = 1; i < times.size(); i++) { + lastAtmPrice = prices.getEntry(spotIndex); + dt = times.get(i - 1) - times.get(i); + double theta = 0.5; + if (i <= 4) { + theta = 1.0; + } + final var theta1 = 1.0 - theta; + final var volSq = impliedVol * impliedVol; + + RealMatrix implicitMatrix, explicitMatrix; + if (isLvPricer) { + implicitMatrix = tridiagMatrix.scalarMultiply(theta * dt); + explicitMatrix = tridiagMatrix.scalarMultiply(-theta1 * dt); + final var localVol = new double[spaceNbOfSteps]; + for (int s = 0; s < spaceNbOfSteps; s++) { + final var lv = volSurface.getLocalVolatility(Math.log(spots.get(s)), times.get(i - 1), + forwardStructure, spaceStepSize, dt); + localVol[s] = lv * lv; + } + + final var volaMatrix = MatrixUtils.createRealDiagonalMatrix(localVol); + implicitMatrix = volaMatrix.multiply(implicitMatrix); + explicitMatrix = volaMatrix.multiply(explicitMatrix); + } else { + implicitMatrix = tridiagMatrix.scalarMultiply(theta * dt * volSq); + explicitMatrix = tridiagMatrix.scalarMultiply(-theta1 * dt * volSq); + } + + implicitMatrix = idMatrix.add(implicitMatrix); + explicitMatrix = idMatrix.add(explicitMatrix); + + if (option.isAmericanOption()) { + // Use the penalty algorithm from Forsyth's 2001 paper to solve the + // linear complementary problem for the American exercise feature. + final var penaltyFactor = 1 / Math.min(timeStepSize * timeStepSize, spaceStepSize * spaceStepSize); + forward = forwardStructure.getForward(times.get(i)); + fdf = forwardStructure.getFutureDividendFactor(times.get(i)); + final var discountFactor = discountCurve.getForwardDiscountFactor(times.get(i), expiryTime); + final var payoffs = MatrixUtils.createRealVector(new double[spaceNbOfSteps]); + final var penaltyMatrix = MatrixUtils.createRealMatrix(spaceNbOfSteps, spaceNbOfSteps); + for (int j = 1; j < spaceNbOfSteps - 1; j++) { + final var payoff = option.getPayoff((forward - fdf) * spots.get(j) + fdf) / discountFactor; + payoffs.setEntry(j, payoff); + penaltyMatrix.setEntry(j, j, prices.getEntry(j) < payoff ? penaltyFactor : 0); + } + + final var b = explicitMatrix.operate(prices); + var oldPrices = prices.copy(); + final var oldPenaltyMatrix = penaltyMatrix.copy(); + final var tol = 1 / penaltyFactor; + int iterations = 0; + while (true) { + assert iterations++ < 100 : "Penalty algorithm for american exercise did not converge in 100 steps"; + final var c = b.add(penaltyMatrix.operate(payoffs)); + final var A = implicitMatrix.add(penaltyMatrix); + final DecompositionSolver solver = new LUDecomposition(A).getSolver(); + prices = solver.solve(c); + for (int j = 1; j < spaceNbOfSteps - 1; j++) { + penaltyMatrix.setEntry(j, j, prices.getEntry(j) < payoffs.getEntry(j) ? penaltyFactor : 0); + } + + if (penaltyMatrix.equals(oldPenaltyMatrix) || (prices.subtract(oldPrices).getLInfNorm()) + / Math.max(oldPrices.getLInfNorm(), 1.0) < tol) { + break; + } + oldPrices = prices.copy(); + } + + } else { + // Solve the PDE step directly + prices = explicitMatrix.operate(prices); + final DecompositionSolver solver = new LUDecomposition(implicitMatrix).getSolver(); + prices = solver.solve(prices); + } + + // Set boundary conditions + prices.setEntry(0, option.getPayoff((forward - fdf) * spaceMinForwardMultiple + fdf)); + prices.setEntry(spaceNbOfSteps - 1, option.getPayoff((forward - fdf) * spaceMaxForwardMultiple + fdf)); + } + } + + final var discountFactor = discountCurve.getDiscountFactor(expiryDate); + final var price = discountFactor * prices.getEntry(spotIndex); + + if (calculateSensis) { + final var dFdX = forwardStructure.getDividendAdjustedStrike(forwardStructure.getForward(expiryDate), + expiryDate); + final var dFdS = forwardStructure.getGrowthDiscountFactor(valDate, expiryDate); + final var delta = discountFactor * 0.5 * (prices.getEntry(spotIndex + 1) - prices.getEntry(spotIndex - 1)) + / spaceStepSize * dFdS / dFdX; + final var gamma = discountFactor + * (prices.getEntry(spotIndex + 1) + prices.getEntry(spotIndex - 1) - 2 * prices.getEntry(spotIndex)) + / spaceStepSq * dFdS * dFdS / dFdX / dFdX; + final var discountFactorTheta = discountCurve.getDiscountFactor(expiryTime - dt); + final var theta = (discountFactorTheta * lastAtmPrice - price) / dt; + return new double[] { price, delta, gamma, theta }; + } else { + return new double[] { price, Double.NaN, Double.NaN, Double.NaN }; + } + } + + public double getImpliedVolatility(Option option, EquityForwardStructure forwardStructure, YieldCurve discountCurve, + double price) { + double initialGuess = 0.25; + final var forward = forwardStructure.getForward(option.getExpiryDate()); + // Use analytic pricer as initial guess for Europeans and OTM Americans + // Use two bisection steps for ITM Americans + if (option.isAmericanOption() && option.getPayoff(forward) > 0.0) { + final var bisectionSolver = new BisectionSearch(0.00001, 1.0); + for (int i = 0; i < 3; i++) { + final double currentVol = bisectionSolver.getNextPoint(); + final double currentPrice = getPrice(option, forwardStructure, discountCurve, + new FlatVolatilitySurface(currentVol)); + + bisectionSolver.setValue(currentPrice - price); + } + initialGuess = bisectionSolver.getBestPoint(); + } else { + final var anaPricer = new AnalyticOptionValuation(dayCounter); + Option testOption; + if (option.isAmericanOption()) { + testOption = new EuropeanOption(option.getExpiryDate(), option.getStrike(), option.isCallOption()); + } else { + testOption = option; + } + initialGuess = anaPricer.getImpliedVolatility(testOption, forwardStructure, discountCurve, price); + + } + + // Solve for implied vol + final var solver = new SecantMethod(initialGuess, initialGuess * 1.01); + while (solver.getAccuracy() / price > 1e-3 && !solver.isDone()) { + final double currentVol = solver.getNextPoint(); + final double currentPrice = getPrice(option, forwardStructure, discountCurve, + new FlatVolatilitySurface(currentVol)); + + solver.setValue(currentPrice - price); + } + + return Math.abs(solver.getBestPoint()); // Note that the PDE only uses sigma^2 + } } diff --git a/src/test/java/net/finmath/equities/AnalyticOptionValuationTest.java b/src/test/java/net/finmath/equities/AnalyticOptionValuationTest.java index 352eec5b9e..43ed41f4b8 100644 --- a/src/test/java/net/finmath/equities/AnalyticOptionValuationTest.java +++ b/src/test/java/net/finmath/equities/AnalyticOptionValuationTest.java @@ -1,24 +1,24 @@ package net.finmath.equities; import static org.junit.Assert.assertEquals; - import java.text.DecimalFormat; import java.time.LocalDate; - +import java.util.ArrayList; import org.junit.Test; - import net.finmath.equities.marketdata.AffineDividend; import net.finmath.equities.marketdata.AffineDividendStream; import net.finmath.equities.marketdata.FlatYieldCurve; +import net.finmath.equities.marketdata.VolatilityPoint; +import net.finmath.equities.marketdata.YieldCurve; import net.finmath.equities.models.BuehlerDividendForwardStructure; import net.finmath.equities.models.FlatVolatilitySurface; +import net.finmath.equities.models.SviVolatilitySurface; import net.finmath.equities.pricer.AnalyticOptionValuation; import net.finmath.equities.products.EuropeanOption; import net.finmath.exception.CalculationException; import net.finmath.time.daycount.DayCountConvention; import net.finmath.time.daycount.DayCountConventionFactory; - /** * Tests for the analytic option pricer under a Black-Scholes process with Buehler dividends. * @@ -26,137 +26,208 @@ */ public class AnalyticOptionValuationTest { - /* - */ - private static final DecimalFormat decform = new DecimalFormat("#0.00"); - private static final DayCountConvention dcc = DayCountConventionFactory.getDayCountConvention("act/365") ; - - @Test - public void Test_noArbitrage() throws CalculationException - { - System.out.println("AnalyticOptionPricer: Test for arbitrage"); - System.out.println("========================================"); - - final var pricer = new AnalyticOptionValuation(dcc); - final var valDate = LocalDate.parse("2019-06-15"); - final var spot = 100.0; - final var volatility = 0.25; - final var flatVol = new FlatVolatilitySurface(volatility); - final var rate = 0.01; - final var curve = new FlatYieldCurve(valDate, rate, dcc); - - final var dividends = new AffineDividendStream(new AffineDividend[] - {new AffineDividend(LocalDate.parse("2020-09-17"), 10.0, 0.0), - new AffineDividend(LocalDate.parse("2021-09-17"), 10.0, 0.0),}); - - final var fwdStructure = new BuehlerDividendForwardStructure(valDate, spot, curve, dividends, dcc); - - final var expiryDateBefore = LocalDate.parse("2020-09-16"); - final var strikeBefore = 100.0; - final var expiryDateAfter = LocalDate.parse("2020-09-17"); - final var strikeAfter = 90.0; - - final boolean[] callput = {true, false}; - for (final var isCall : callput) - { - final var optionBefore = new EuropeanOption(expiryDateBefore, strikeBefore, isCall); - final var optionAfter = new EuropeanOption(expiryDateAfter, strikeAfter, isCall); - - final var priceBefore = pricer.getPrice(optionBefore, fwdStructure, curve, flatVol); - final var priceAfter = pricer.getPrice(optionAfter, fwdStructure, curve, flatVol); - - - final var volBefore = pricer.getImpliedVolatility(optionBefore, fwdStructure, curve, priceBefore); - final var volAfter = pricer.getImpliedVolatility(optionAfter, fwdStructure, curve, priceAfter); - - //System.out.println("BS Price " + (isCall ? "Call" : "Put") + " before: " + bsPrice); - System.out.println("Price before: " + priceBefore); - System.out.println("Price after: " + priceAfter); - System.out.println("Implied vol before: " + volBefore); - System.out.println("Implied vol after: " + volAfter); - System.out.println(); - - assertEquals("Price before and after dividend should be almost equal", - 0.0, priceAfter/priceBefore - 1.0, 0.005); - assertEquals("Implied vol before dividend deviates from input vol", - 0.0, volBefore/volatility -1.0, 1E-14); - assertEquals("Implied vol after dividend deviates from input vol", - 0.0, volAfter/volatility -1.0, 1E-14); - } - } - - @Test - public void Test_sensis() throws CalculationException - { - System.out.println("AnalyticOptionPricer: Test Greeks"); - System.out.println("================================="); - - final var pricer = new AnalyticOptionValuation(dcc); - final var valDate = LocalDate.parse("2019-06-15"); - final var spot = 100.0; - final var volatility = 0.35; - final var flatVol = new FlatVolatilitySurface(volatility); - final var rate = 0.15; - final var discountCurve = new FlatYieldCurve(valDate, rate, dcc); - final var repoRate = 0.25; - final var repoCurve = new FlatYieldCurve(valDate, repoRate, dcc); - - final var dividends = new AffineDividendStream(new AffineDividend[] - {new AffineDividend(LocalDate.parse("2020-09-17"), 10.0, 0.0), - new AffineDividend(LocalDate.parse("2021-09-17"), 10.0, 0.0),}); - - final var fwdStructure = new BuehlerDividendForwardStructure(valDate, spot, repoCurve, dividends, dcc); - - final var expiryDate = LocalDate.parse("2020-12-15"); - final var strike = 90.0; - - final boolean[] callput = {true, false}; - for (final var isCall : callput) - { - final var option = new EuropeanOption(expiryDate, strike, isCall); - - final var price = pricer.getPrice(option, fwdStructure, discountCurve, flatVol); - final var spotStep = spot * 0.01; - final var priceUp = pricer.getPrice(option, fwdStructure.cloneWithNewSpot(spot + spotStep), discountCurve, flatVol); - final var priceDown = pricer.getPrice(option, fwdStructure.cloneWithNewSpot(spot - spotStep), discountCurve, flatVol); - - final var anaDelta = pricer.getDelta(option, fwdStructure, discountCurve, flatVol); - final var anaGamma = pricer.getGamma(option, fwdStructure, discountCurve, flatVol); - final var fdDelta = 0.5 * (priceUp - priceDown) / spotStep; - final var fdGamma = (priceUp + priceDown - 2 * price) / spotStep / spotStep; - - final var volStep = 0.0001; - final var priceVega = pricer.getPrice(option, fwdStructure, discountCurve, flatVol.getShiftedSurface(volStep)); - final var anaVega = pricer.getVega(option, fwdStructure, discountCurve, flatVol); - final var fdVega = (priceVega - price) / volStep; - - final var anaTheta = pricer.getTheta(option, fwdStructure, discountCurve, flatVol); - final var thetaDate = valDate.plusDays(1); - final var thetaSpot = fwdStructure.getForward(thetaDate); - final var thetaCurve = discountCurve.rollToDate(thetaDate); - final var shiftedFwdStructure = fwdStructure.cloneWithNewSpot(thetaSpot).cloneWithNewDate(thetaDate); - final var priceTheta = pricer.getPrice(option, shiftedFwdStructure, thetaCurve, flatVol); - final var fdTheta = (priceTheta - price) / dcc.getDaycountFraction(valDate, thetaDate); - - - System.out.println("Ana "+ (isCall ? "Call" : "Put") + " Delta: " + anaDelta); - System.out.println("FinDiff "+ (isCall ? "Call" : "Put") + " Delta: " + fdDelta); - System.out.println("Ana " + (isCall ? "Call" : "Put") + " Gamma: " + anaGamma); - System.out.println("FinDiff " + (isCall ? "Call" : "Put") + " Gamma: " + fdGamma); - System.out.println("Ana " + (isCall ? "Call" : "Put") + " Vega: " + anaVega); - System.out.println("FinDiff " + (isCall ? "Call" : "Put") + " Vega: " + fdVega); - System.out.println("Ana " + (isCall ? "Call" : "Put") + " Theta: " + anaTheta); - System.out.println("FinDiff " + (isCall ? "Call" : "Put") + " Theta: " + fdTheta); - System.out.println(); - - assertEquals("Analytic Delta formula and finite difference approximation deviate too much.", - 0.0, anaDelta/fdDelta -1.0, 0.01); - assertEquals("Analytic Gamma formula and finite difference approximation deviate too much", - 0.0, anaGamma/fdGamma -1.0, 0.01); - assertEquals("Analytic Vega formula and finite difference approximation deviate too much", - 0.0, anaVega/fdVega -1.0, 0.01); - assertEquals("Analytic Theta formula and finite difference approximation deviate too much", - 0.0, anaTheta/fdTheta -1.0, 0.01); - } - } + + /* + */ + private static final DecimalFormat decform = new DecimalFormat("#0.00"); + private static final DayCountConvention dcc = DayCountConventionFactory.getDayCountConvention("act/365"); + + @Test + public void Test_noArbitrage() throws CalculationException { + System.out.println("AnalyticOptionPricer: Test for arbitrage"); + System.out.println("========================================"); + + final var pricer = new AnalyticOptionValuation(dcc); + final var valDate = LocalDate.parse("2019-06-15"); + final var spot = 100.0; + final var volatility = 0.25; + final var flatVol = new FlatVolatilitySurface(volatility); + final var rate = 0.01; + final var curve = new FlatYieldCurve(valDate, rate, dcc); + + final var dividends = new AffineDividendStream( + new AffineDividend[] { new AffineDividend(LocalDate.parse("2020-09-17"), 10.0, 0.0), + new AffineDividend(LocalDate.parse("2021-09-17"), 10.0, 0.0), }); + + final var fwdStructure = new BuehlerDividendForwardStructure(valDate, spot, curve, dividends, dcc); + + final var expiryDateBefore = LocalDate.parse("2020-09-16"); + final var strikeBefore = 100.0; + final var expiryDateAfter = LocalDate.parse("2020-09-17"); + final var strikeAfter = 90.0; + + final boolean[] callput = { true, false }; + for (final var isCall : callput) { + final var optionBefore = new EuropeanOption(expiryDateBefore, strikeBefore, isCall); + final var optionAfter = new EuropeanOption(expiryDateAfter, strikeAfter, isCall); + + final var priceBefore = pricer.getPrice(optionBefore, fwdStructure, curve, flatVol); + final var priceAfter = pricer.getPrice(optionAfter, fwdStructure, curve, flatVol); + + final var volBefore = pricer.getImpliedVolatility(optionBefore, fwdStructure, curve, priceBefore); + final var volAfter = pricer.getImpliedVolatility(optionAfter, fwdStructure, curve, priceAfter); + + //System.out.println("BS Price " + (isCall ? "Call" : "Put") + " before: " + bsPrice); + System.out.println("Price before: " + priceBefore); + System.out.println("Price after: " + priceAfter); + System.out.println("Implied vol before: " + volBefore); + System.out.println("Implied vol after: " + volAfter); + System.out.println(); + + assertEquals("Price before and after dividend should be almost equal", 0.0, priceAfter / priceBefore - 1.0, + 0.005); + assertEquals("Implied vol before dividend deviates from input vol", 0.0, volBefore / volatility - 1.0, + 1E-14); + assertEquals("Implied vol after dividend deviates from input vol", 0.0, volAfter / volatility - 1.0, 1E-14); + } + } + + @Test + public void Test_sensis() throws CalculationException { + System.out.println("AnalyticOptionPricer: Test Greeks"); + System.out.println("================================="); + + final var pricer = new AnalyticOptionValuation(dcc); + final var valDate = LocalDate.parse("2019-06-15"); + final var spot = 100.0; + final var volatility = 0.35; + final var flatVol = new FlatVolatilitySurface(volatility); + final var rate = 0.15; + final var discountCurve = new FlatYieldCurve(valDate, rate, dcc); + final var repoRate = 0.25; + final var repoCurve = new FlatYieldCurve(valDate, repoRate, dcc); + + final var dividends = new AffineDividendStream( + new AffineDividend[] { new AffineDividend(LocalDate.parse("2020-09-17"), 10.0, 0.0), + new AffineDividend(LocalDate.parse("2021-09-17"), 10.0, 0.0), }); + + final var fwdStructure = new BuehlerDividendForwardStructure(valDate, spot, repoCurve, dividends, dcc); + + final var expiryDate = LocalDate.parse("2020-12-15"); + final var strike = 90.0; + + final boolean[] callput = { true, false }; + for (final var isCall : callput) { + final var option = new EuropeanOption(expiryDate, strike, isCall); + + final var price = pricer.getPrice(option, fwdStructure, discountCurve, flatVol); + final var spotStep = spot * 0.01; + final var priceUp = pricer.getPrice(option, fwdStructure.cloneWithNewSpot(spot + spotStep), discountCurve, + flatVol); + final var priceDown = pricer.getPrice(option, fwdStructure.cloneWithNewSpot(spot - spotStep), discountCurve, + flatVol); + + final var anaDelta = pricer.getDelta(option, fwdStructure, discountCurve, flatVol); + final var anaGamma = pricer.getGamma(option, fwdStructure, discountCurve, flatVol); + final var fdDelta = 0.5 * (priceUp - priceDown) / spotStep; + final var fdGamma = (priceUp + priceDown - 2 * price) / spotStep / spotStep; + + final var volStep = 0.0001; + final var priceVega = pricer.getPrice(option, fwdStructure, discountCurve, + flatVol.getShiftedSurface(volStep)); + final var anaVega = pricer.getVega(option, fwdStructure, discountCurve, flatVol); + final var fdVega = (priceVega - price) / volStep; + + final var anaTheta = pricer.getTheta(option, fwdStructure, discountCurve, flatVol); + final var thetaDate = valDate.plusDays(1); + final var thetaSpot = fwdStructure.getForward(thetaDate); + final var thetaCurve = discountCurve.rollToDate(thetaDate); + final var shiftedFwdStructure = fwdStructure.cloneWithNewSpot(thetaSpot).cloneWithNewDate(thetaDate); + final var priceTheta = pricer.getPrice(option, shiftedFwdStructure, thetaCurve, flatVol); + final var fdTheta = (priceTheta - price) / dcc.getDaycountFraction(valDate, thetaDate); + + System.out.println("Ana " + (isCall ? "Call" : "Put") + " Delta: " + anaDelta); + System.out.println("FinDiff " + (isCall ? "Call" : "Put") + " Delta: " + fdDelta); + System.out.println("Ana " + (isCall ? "Call" : "Put") + " Gamma: " + anaGamma); + System.out.println("FinDiff " + (isCall ? "Call" : "Put") + " Gamma: " + fdGamma); + System.out.println("Ana " + (isCall ? "Call" : "Put") + " Vega: " + anaVega); + System.out.println("FinDiff " + (isCall ? "Call" : "Put") + " Vega: " + fdVega); + System.out.println("Ana " + (isCall ? "Call" : "Put") + " Theta: " + anaTheta); + System.out.println("FinDiff " + (isCall ? "Call" : "Put") + " Theta: " + fdTheta); + System.out.println(); + + assertEquals("Analytic Delta formula and finite difference approximation deviate too much.", 0.0, + anaDelta / fdDelta - 1.0, 0.01); + assertEquals("Analytic Gamma formula and finite difference approximation deviate too much", 0.0, + anaGamma / fdGamma - 1.0, 0.01); + assertEquals("Analytic Vega formula and finite difference approximation deviate too much", 0.0, + anaVega / fdVega - 1.0, 0.01); + assertEquals("Analytic Theta formula and finite difference approximation deviate too much", 0.0, + anaTheta / fdTheta - 1.0, 0.01); + } + } + + @Test + public void Test_complexMarketData() throws CalculationException { + System.out.println("AnalyticOptionPricer: Test with complex market data"); + System.out.println("========================================"); + + final var pricer = new AnalyticOptionValuation(dcc); + final var valDate = LocalDate.parse("2019-06-15"); + final var spot = 100.0; + + LocalDate[] curveDates = new LocalDate[] { LocalDate.parse("2019-09-15"), LocalDate.parse("2019-12-15"), + LocalDate.parse("2020-03-15"), LocalDate.parse("2020-06-15"), LocalDate.parse("2020-12-15"), + LocalDate.parse("2021-06-15") }; + final var discountCurve = new YieldCurve("discount", valDate, dcc, curveDates, + new double[] { 0.991216881, 0.983837517, 0.977731147, 0.970365774, 0.959480762, 0.951164274 }); + + final var repoCurve = new YieldCurve("discount", valDate, dcc, curveDates, + new double[] { 0.988721617, 0.978917197, 0.970418946, 0.960684153, 0.945157112, 0.932304417 }); + final var dividends = new AffineDividendStream( + new AffineDividend[] { new AffineDividend(LocalDate.parse("2020-09-17"), 10.0, 0.0), + new AffineDividend(LocalDate.parse("2021-09-17"), 10.0, 0.0), }); + final var fwdStructure = new BuehlerDividendForwardStructure(valDate, spot, repoCurve, dividends, dcc); + + final var volaPoints = new ArrayList(); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2019-11-03"), 60, 0.4076)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2019-11-03"), 80, 0.2696)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2019-11-03"), 100, 0.1632)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2019-11-03"), 120, 0.1315)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2019-11-03"), 140, 0.1778)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2020-05-03"), 60, 0.3282)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2020-05-03"), 80, 0.2357)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2020-05-03"), 100, 0.1661)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2020-05-03"), 120, 0.1220)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2020-05-03"), 140, 0.1323)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2020-11-01"), 60, 0.2960)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2020-11-01"), 80, 0.2253)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2020-11-01"), 100, 0.1715)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2020-11-01"), 120, 0.1299)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2020-11-01"), 140, 0.1234)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2021-05-01"), 60, 0.2774)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2021-05-01"), 80, 0.2193)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2021-05-01"), 100, 0.1759)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2021-05-01"), 120, 0.1381)); + volaPoints.add(new VolatilityPoint(LocalDate.parse("2021-05-01"), 140, 0.1264)); + final var volSurface = new SviVolatilitySurface(dcc, false); + volSurface.calibrate(fwdStructure, volaPoints); + + final var expiryDateBefore = LocalDate.parse("2020-09-16"); + final var strikeBefore = 100.0; + final var expiryDateAfter = LocalDate.parse("2020-09-17"); + final var strikeAfter = 90.0; + + final boolean[] callput = { true, false }; + for (final var isCall : callput) { + final var optionBefore = new EuropeanOption(expiryDateBefore, strikeBefore, isCall); + final var optionAfter = new EuropeanOption(expiryDateAfter, strikeAfter, isCall); + + final var priceBefore = pricer.getPrice(optionBefore, fwdStructure, discountCurve, volSurface); + final var priceAfter = pricer.getPrice(optionAfter, fwdStructure, discountCurve, volSurface); + + final var volBefore = pricer.getImpliedVolatility(optionBefore, fwdStructure, discountCurve, priceBefore); + final var volAfter = pricer.getImpliedVolatility(optionAfter, fwdStructure, discountCurve, priceAfter); + + //System.out.println("BS Price " + (isCall ? "Call" : "Put") + " before: " + bsPrice); + System.out.println("Price before: " + priceBefore); + System.out.println("Price after: " + priceAfter); + System.out.println("Implied vol before: " + volBefore); + System.out.println("Implied vol after: " + volAfter); + System.out.println(); + + assertEquals("Price before and after dividend should be almost equal", 0.0, priceAfter / priceBefore - 1.0, + 0.005); + } + } } diff --git a/src/test/java/net/finmath/equities/SviVolatiltitySurfaceTest.java b/src/test/java/net/finmath/equities/SviVolatiltitySurfaceTest.java index 4b852af0ba..b4e1f24910 100644 --- a/src/test/java/net/finmath/equities/SviVolatiltitySurfaceTest.java +++ b/src/test/java/net/finmath/equities/SviVolatiltitySurfaceTest.java @@ -20,7 +20,7 @@ import net.finmath.time.daycount.DayCountConventionFactory; /** - * Tests for the SVI volatility surface implementation. + * Tests for the SVI volatility surface implementation * * @author Andreas Grotz */