/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.bbg.util; import static org.threeten.bp.DayOfWeek.FRIDAY; import java.util.ArrayList; import java.util.List; import java.util.NavigableSet; import java.util.Set; import java.util.TreeSet; import org.threeten.bp.LocalDate; import org.threeten.bp.ZonedDateTime; import com.google.common.base.Objects; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.opengamma.financial.convention.daycount.ActualActualISDA; import com.opengamma.financial.security.option.OptionType; import com.opengamma.id.ExternalId; import com.opengamma.util.OpenGammaClock; /** * <p> * Class for narrowing an option chain by expiry, strike, and option type. Typically, this class is initialized with the output * of {@link com.opengamma.bbg.util.BloombergDataUtils#getOptionChain(org.slf4j.Logger, com.opengamma.bbg.ReferenceDataProvider, String) BloombergDataUtil.getOptionChain()}. * The caller then uses the different {@code narrow*} methods to select the desired option or options. In this way, the caller can pick * very specific options before looking up their information in Bloomberg. * </p> * <p> * Each instance of {@code BloombergEQVanillaOptionChain} is immutable. Each of the {@code narrow*} methods returns a new immutable instance * of {@code BloombergEQVanillaOptionChain}. The chain is a wrapper around a list of {@link com.opengamma.id.ExternalId Identifiers}, which * are obtainable via a getter method after narrowing is complete. This getter method returns the {@code Identifiers} as a {@code Set}, in order * to mimic the behavior of {@code BloombergDataUtil.getOptionChain()}. Different instances of the chain may share instances of the contained * {@code Identifiers}, but these are themselves immutable, so this is completely safe. * </p> * <p> * The above setup works well with a method-chaining idiom. For example, here is how you would obtain a single call option that expires 2 * or more months from the present, and is within 1 strike of the current price: * </p> * <pre> double currentPrice = . . . ; Set<Identifier> identifiers = BloombergDataUtil.getOptionChain( . . . .); BloombergEQVanillaOptionChain chain = new BloombergEQVanillaOptionChain (identifiers); LocalDate today = LocalDate.now(); Identifier desiredOption = chain.narrowByExpiry(today, 2).narrowByStrike(currentPrice, 1). narrowByOptionType(OptionType.CALL).getIdentifiers().first(); * </pre> * <p> * To get both the put and call options, one could reuse a chain instance, like so: * </p> * <pre> double currentPrice = . . . ; Set<Identifier> identifiers = BloombergDataUtil.getOptionChain( . . . .); BloombergEQVanillaOptionChain chain = new BloombergEQVanillaOptionChain (identifiers); LocalDate today = LocalDate.now(); BloombergEQVanillaOptionChain optionPair = chain.narrowByExpiry(today, 2).narrowByStrike(currentPrice, 1); Identifier desiredCall = optionPair.narrowByOptionType(OptionType.CALL).getIdentifiers().first(); Identifier desiredPut = optionPair.narrowByOptionType(OptionType.PUT).getIdentifiers().first(); * </pre> * <p> * The narrow methods are <b>guaranteed</b> to return a chain with at least one element, <b>unless</b> the * chain was built around an empty {@code Identifier} set to begin with, in which case the methods will return * an empty, non-null chain. * </p> * Internally, this class uses {@link BloombergTickerParserEQVanillaOption}. */ public class BloombergEQVanillaOptionChain { // ------------ FIELDS ------------ private List<ExternalId> _identifiers; private ActualActualISDA _dayCount = new ActualActualISDA(); // ------------ METHODS ------------ // -------- CONSTRUCTORS -------- /** * <p> * Create an option chain around a {@link java.util.Set} of identifiers. Typically, these come from the result of * a call to {@link com.opengamma.bbg.util.BloombergDataUtils#getOptionChain BloombergDataUtil.getOptionChain()}. * </p> * <p> * The identifiers must have an {@link com.opengamma.id.ExternalScheme} of {@link com.opengamma.core.id.ExternalSchemes#BLOOMBERG_TICKER}. * </p> * @param identifiers the identifiers that comprise the chain */ public BloombergEQVanillaOptionChain(Set<ExternalId> identifiers) { _identifiers = new ArrayList<ExternalId>(identifiers); } /** * <p> * Create an option chain around a {@link java.util.List} of identifiers. Typically, these come from the result of * a call to {@link com.opengamma.bbg.util.BloombergDataUtils#getOptionChain BloombergDataUtil.getOptionChain()}. * </p> * <p> * The identifiers must have an {@link com.opengamma.id.ExternalScheme} of {@link com.opengamma.core.id.ExternalSchemes#BLOOMBERG_TICKER}. * </p> * @param identifiers the identifiers that comprise the chain */ public BloombergEQVanillaOptionChain(List<ExternalId> identifiers) { _identifiers = identifiers; } /** * Creates a copy of a {@code BloombergEQVanillaOptionChain}. Note that this is a shallow * copy; since both {@code Identifier} and {@code BloombergEQVanillaOptionChain} are immutable, * this is not an issue. * @param original the original chain to copy */ public BloombergEQVanillaOptionChain(BloombergEQVanillaOptionChain original) { _identifiers = original._identifiers; } // -------- MAIN OPERATIONS -------- /** * Returns a new chain narrowed by option type (either {@link com.opengamma.financial.security.option.OptionType#CALL CALL} * or {@link com.opengamma.financial.security.option.OptionType#PUT PUT}). * @param optionType the option type to narrow on * @return a new chain narrowed by option type */ public BloombergEQVanillaOptionChain narrowByOptionType(final OptionType optionType) { // Simple O(n) filtering on optiontype List<ExternalId> result = new ArrayList<ExternalId>(Collections2.filter(_identifiers, new Predicate<ExternalId>() { public boolean apply(ExternalId identifier) { return new BloombergTickerParserEQVanillaOption(identifier).getOptionType().equals(optionType); } })); return new BloombergEQVanillaOptionChain(result); } /** * Returns a new chain narrowed by expiry. The expiry is specified as being at least {@code monthsFromToday} months from * today. See {@link #narrowByExpiry(LocalDate, int)} for details of the algorithm. * @param monthsFromToday number of months from today to start searching for the expiry * @return a new chain narrowed by expiry */ public BloombergEQVanillaOptionChain narrowByExpiry(int monthsFromToday) { return narrowByExpiry(LocalDate.now(OpenGammaClock.getInstance()), monthsFromToday); } /** * <p> * Returns a new chain narrowed by expiry. The expiry is specified as being at least {@code monthsFromToday} months from * an arbitrary reference date. Both positive and negative month offsets are supported. * </p> * The search algorithm is as follows: * <ol> * <li>Search the chain for an expiry date exactly {@code monthsFromReferenceDate} months from the reference date. An expiry date is * defined as the Saturday after the third Friday of the month. If {@code monthsFromReferenceDate} is {@code 0}, then the algorithm * will start in month of the reference date, but will skip the expiry if it has already passed.</li> * <li>If options with the above expiry were found, then the algorithm stops, and the resulting chain is returned.</li> * <li>If not, then the algorithm will find the expiry that is nearest to the exact expiry from step 1. It measures the difference using * actual days. The resulting chain is then returned. * </ol> * <p> * Below, we provide a table showing sample results. We use US date format for Bloomberg compatibility. We cover only ≥ 0 offsets * since that is the more usual case. Assume that: * <ul> * <li>Reference date is 4/18/2011</li> * <li>The option chain has expiries of 4/16/2011, 5/21/2011, 6/18/2011, 7/16/2011, 10/22/2011, 1/21/2012, 1/19/2013</li> * </ul> * <table> * <tr><th>Requested offset</th><th>Resulting Expiry</th></tr> * <tr><td>0</td><td>05/21/2011</td></tr> * <tr><td>1</td><td>05/21/2011</td></tr> * <tr><td>2</td><td>06/18/2011</td></tr> * <tr><td>3</td><td>07/16/2011</td></tr> * <tr><td>4</td><td>07/16/2011</td></tr> * <tr><td>5</td><td>10/22/2011</td></tr> * <tr><td>6</td><td>10/22/2011</td></tr> * <tr><td>7</td><td>10/22/2011</td></tr> * <tr><td>8</td><td>01/21/2012</td></tr> * <tr><td>9</td><td>01/21/2012</td></tr> * <tr><td>10</td><td>01/21/2012</td></tr> * <tr><td>11</td><td>01/21/2012</td></tr> * <tr><td>12</td><td>01/21/2012</td></tr> * <tr><td>13</td><td>01/21/2012</td></tr> * <tr><td>14</td><td>01/21/2012</td></tr> * <tr><td>15</td><td>01/21/2012</td></tr> * <tr><td>16</td><td>01/19/2013</td></tr> * <tr><td>17</td><td>01/19/2013</td></tr> * <tr><td>18</td><td>01/19/2013</td></tr> * <tr><td>19</td><td>01/19/2013</td></tr> * <tr><td>20</td><td>01/19/2013</td></tr> * </table> * <p> * Notice how, for the {@code 0} offset, the result was {@code 05/21/2011} and not {@code 04/16/2011}. That is because the reference date * was {@code 04/18/2011}, which is after {@code 04/16/2011}. If the reference date had been, for example, {@code 04/11/2011}, then the * result would have been {@code 4/16/2011} for a {@code 0} offset. * </p> * @param referenceDate the date to which the expiry is in reference * @param monthsFromReferenceDate number of months from the reference date to start searching for the expiry * @return a new chain narrowed by expiry */ public BloombergEQVanillaOptionChain narrowByExpiry(LocalDate referenceDate, final int monthsFromReferenceDate) { // Special case - return empty on empty input if (_identifiers.isEmpty()) { return new BloombergEQVanillaOptionChain(_identifiers); } // Create parser version of chain // Collect all unique property values from chain List<BloombergTickerParserEQVanillaOption> parsers = new ArrayList<BloombergTickerParserEQVanillaOption>(_identifiers.size()); NavigableSet<LocalDate> expiries = new TreeSet<LocalDate>(); for (ExternalId identifier : _identifiers) { BloombergTickerParserEQVanillaOption parser = new BloombergTickerParserEQVanillaOption(identifier); parsers.add(parser); LocalDate expiry = parser.getExpiry(); expiries.add(expiry); } // Find the desired expiry LocalDate thirdSaturdayOfTargetMonth = determineTargetExpiry(referenceDate, monthsFromReferenceDate); LocalDate floorValue = Objects.firstNonNull(expiries.floor(thirdSaturdayOfTargetMonth), expiries.first()); LocalDate targetValue = floorValue; if (!floorValue.equals(thirdSaturdayOfTargetMonth)) { LocalDate ceilingValue = Objects.firstNonNull(expiries.ceiling(thirdSaturdayOfTargetMonth), expiries.last()); double diff1 = calcDayDiff(thirdSaturdayOfTargetMonth, floorValue); double diff2 = calcDayDiff(thirdSaturdayOfTargetMonth, ceilingValue); if (diff1 < diff2) { targetValue = floorValue; } else { targetValue = ceilingValue; } } // Collect all identifiers with this expiry // (Note: tried pre-hashing and retrieving, was slower than this O(n) approach) List<ExternalId> identifiers = new ArrayList<ExternalId>(); for (BloombergTickerParserEQVanillaOption parser : parsers) { if (parser.getExpiry().equals(targetValue)) { identifiers.add(parser.getIdentifier()); } } // Done return new BloombergEQVanillaOptionChain(identifiers); } /** * <p> * Returns a new chain narrowed by strike. The strike is specified as being at least {@code strikeOffset} strikes from * an arbitrary reference price (usually the current underlyer price.) Both positive and negative strike offsets are supported. * </p> * <p> * The search algorithm is as follows: * </p> * <ol> * <li>If the {@code strikeOffset} is {@code 0}, return the strike that is nearest to the reference price, whether it is above or below.</li> * <li>For non-zero {@code strikeOffset} values, simply find the place in the chain where the reference price would sit, and then * find the strike that is {@code strikeOffset} strikes away from it. Return a new option chain with all options of the resulting * strike.</li> * <li>In either case, if the algorithm reaches the beginning or end of the chain, the strike at the beginning or end will be chosen. </li> * </ol> * <p> * Below, we provide a table showing sample results. We cover only ≥ 0 offsets * since that is the more usual case. Assume that: * <ul> * <li>Reference price is 199</li> * <li>The option chain has strikes of 135, 140, 145, . . . 540 at intervals of 5.</li> * </ul> * <table> * <tr><th>Requested Offset</th><th>Resulting Strike</th></tr> * <tr><td>0</td><td>200.0</td></tr> * <tr><td>1</td><td>200.0</td></tr> * <tr><td>2</td><td>205.0</td></tr> * <tr><td>3</td><td>210.0</td></tr> * <tr><td>4</td><td>215.0</td></tr> * <tr><td>5</td><td>220.0</td></tr> * <tr><td>6</td><td>225.0</td></tr> * <tr><td>7</td><td>230.0</td></tr> * <tr><td>8</td><td>235.0</td></tr> * <tr><td>9</td><td>240.0</td></tr> * <tr><td>10</td><td>245.0</td></tr> * <tr><td>...</td><td>...</td></tr> * <tr><td>68</td><td>535.0</td></tr> * <tr><td>69</td><td>540.0</td></tr> * <tr><td>70</td><td>540.0</td></tr> * <tr><td>71</td><td>540.0</td></tr> * <tr><td>72</td><td>540.0</td></tr> * <tr><td>73</td><td>540.0</td></tr> * <tr><td>74</td><td>540.0</td></tr> * <tr><td>75</td><td>540.0</td></tr> * </table> * <p> * Notice how, for both the {@code 0} and {@code 1} offsets, the result was {@code 200}. For the {@code 0} offset, the algorithm simply * looked for the strike nearest to {@code 199}; for the {@code 1} offset, the algorithm found the next strike above {@code 199}. In both * cases, that value was {@code 200}. Notice also that after {@code 70+} offsets, we reach the end of the chain, and the resulting strike is * always {@code 540}. * </p> * @param referencePrice the price to which the strike offset is in reference * @param strikeOffset distance of strikes from the reference price to select * @return a new chain narrowed by strike */ public BloombergEQVanillaOptionChain narrowByStrike(final double referencePrice, int strikeOffset) { // Create parser version of chain // Collect all unique property values from chain List<BloombergTickerParserEQVanillaOption> parsers = new ArrayList<BloombergTickerParserEQVanillaOption>(_identifiers.size()); NavigableSet<Double> strikes = new TreeSet<Double>(); for (ExternalId identifier : _identifiers) { BloombergTickerParserEQVanillaOption parser = new BloombergTickerParserEQVanillaOption(identifier); parsers.add(parser); Double strike = parser.getStrike(); strikes.add(strike); } // Find the desired strike Double targetValue = null; // Special case: offset is 0 to begin with, just find the nearest strike if (strikeOffset == 0) { double floorValue = Objects.firstNonNull(strikes.floor(referencePrice), strikes.first()); targetValue = floorValue; if (floorValue != referencePrice) { double ceilingValue = Objects.firstNonNull(strikes.ceiling(referencePrice), strikes.last()); double diff1 = Math.abs(referencePrice - floorValue); double diff2 = Math.abs(referencePrice - ceilingValue); if (diff1 < diff2) { targetValue = floorValue; } else { targetValue = ceilingValue; } } } else { // Otherwise, move forward or backward the desired number of offsets and take that value targetValue = referencePrice; while (strikeOffset != 0) { Double next = null; if (strikeOffset > 0) { next = strikes.higher(targetValue); if (next != null) { targetValue = next; strikeOffset--; } else { break; } } else { next = strikes.lower(targetValue); if (next != null) { targetValue = next; strikeOffset++; } else { break; } } } } // Collect all identifiers with this strike // (Note: tried pre-hashing and retrieving, was slower than this O(n) approach) List<ExternalId> identifiers = new ArrayList<ExternalId>(); for (BloombergTickerParserEQVanillaOption parser : parsers) { if (parser.getStrike() == targetValue) { identifiers.add(parser.getIdentifier()); } } // Done return new BloombergEQVanillaOptionChain(identifiers); } // -------- PROPERTIES -------- /** * Returns the {@code Identifiers} in the chain. They will be in a set sorted by alphabetical order, in order to mimic the output ordering * of {@link com.opengamma.bbg.util.BloombergDataUtils#getOptionChain BloombergDataUtil.getOptionChain()}. * @return the {@code Identifiers} in the chain */ public NavigableSet<ExternalId> getIdentifiers() { return new TreeSet<ExternalId>(_identifiers); } // -------- PRIVATE SUBROUTINES -------- private LocalDate determineTargetExpiry(LocalDate referenceDate, int monthsFromReferenceDate) { LocalDate result = referenceDate.plusMonths(monthsFromReferenceDate); result = LocalDate.of(result.getYear(), result.getMonth(), 1); while (!(result.getDayOfWeek() == FRIDAY)) { result = result.plusDays(1); } result = result.plusDays(15); // Saturday after third Friday // Fencepost condition: if we are looking 0 months ahead, but the referenceDate is past the // the Saturday after third Friday, then move forward 1 month if (monthsFromReferenceDate == 0 && result.isBefore(referenceDate)) { result = determineTargetExpiry(referenceDate, 1); } return result; } private double calcDayDiff(LocalDate thirdSaturdayOfTargetMonth, LocalDate expiry) { ZonedDateTime dummyNow = ZonedDateTime.now(OpenGammaClock.getInstance()); // "now()" is just to get dummy time of day and zone ZonedDateTime zonedThirdSaturdayOfTargetMonth = thirdSaturdayOfTargetMonth.atTime(dummyNow.toLocalTime()).atZone(dummyNow.getZone()); ZonedDateTime zonedExpiry = expiry.atTime(dummyNow.toLocalTime()).atZone(dummyNow.getZone()); if (expiry.isAfter(thirdSaturdayOfTargetMonth)) { return _dayCount.getDayCountFraction(zonedThirdSaturdayOfTargetMonth, zonedExpiry); } else { return _dayCount.getDayCountFraction(zonedExpiry, zonedThirdSaturdayOfTargetMonth); } } }