com.opengamma.examples.bloomberg.loader.DemoEquityOptionCollarPortfolioLoader.java Source code

Java tutorial

Introduction

Here is the source code for com.opengamma.examples.bloomberg.loader.DemoEquityOptionCollarPortfolioLoader.java

Source

/**
 * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
 *
 * Please see distribution for license.
 */
package com.opengamma.examples.bloomberg.loader;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.fudgemsg.FudgeMsg;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.MessageFormatter;
import org.threeten.bp.LocalDate;
import org.threeten.bp.Period;

import com.google.common.collect.BiMap;
import com.google.common.collect.Sets;
import com.opengamma.OpenGammaRuntimeException;
import com.opengamma.bbg.BloombergConstants;
import com.opengamma.bbg.BloombergFields;
import com.opengamma.bbg.referencedata.ReferenceDataProvider;
import com.opengamma.bbg.util.BloombergDataUtils;
import com.opengamma.bbg.util.BloombergTickerParserEQOption;
import com.opengamma.component.tool.AbstractTool;
import com.opengamma.core.historicaltimeseries.HistoricalTimeSeries;
import com.opengamma.core.id.ExternalSchemes;
import com.opengamma.financial.security.equity.EquitySecurity;
import com.opengamma.financial.security.equity.GICSCode;
import com.opengamma.financial.security.option.OptionType;
import com.opengamma.id.ExternalId;
import com.opengamma.id.ExternalIdBundle;
import com.opengamma.id.UniqueId;
import com.opengamma.integration.tool.IntegrationToolContext;
import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesInfoDocument;
import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesInfoSearchRequest;
import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesInfoSearchResult;
import com.opengamma.master.portfolio.ManageablePortfolio;
import com.opengamma.master.portfolio.ManageablePortfolioNode;
import com.opengamma.master.portfolio.PortfolioDocument;
import com.opengamma.master.portfolio.PortfolioMaster;
import com.opengamma.master.portfolio.PortfolioSearchRequest;
import com.opengamma.master.portfolio.PortfolioSearchResult;
import com.opengamma.master.position.ManageablePosition;
import com.opengamma.master.position.ManageableTrade;
import com.opengamma.master.position.PositionDocument;
import com.opengamma.master.security.ManageableSecurity;
import com.opengamma.master.security.SecuritySearchRequest;
import com.opengamma.master.security.SecuritySearchResult;
import com.opengamma.scripts.Scriptable;
import com.opengamma.util.tuple.Pair;

/**
 * A portfolio loader which generates a sensible portfolio of liquid equities and options on them Also see DemoEquityOptionPortfolioLoader.
 */
@Scriptable
public class DemoEquityOptionCollarPortfolioLoader extends AbstractTool<IntegrationToolContext> {

    private static final String TOOL_NAME = "Demo Equity Option Portfolio Loader";
    private static final String PORTFOLIO_NAME_OPT = "p";
    private static final String OPTION_DEPTH_OPT = "d";
    private static final String NUM_CONTRACTS_OPT = "n";
    private static final String NUM_INDEX_MEMBERS_OPT = "m";

    private static final String BLOOMBERG_EQUITY_TICKER_SUFFIX = " Equity";

    private static final Logger s_logger = LoggerFactory.getLogger(DemoEquityOptionCollarPortfolioLoader.class);

    private static final Map<String, String> INDEXES_TO_EXCHANGE = getIndexToExchangeMap();
    private static final Set<String> EXCLUDED_SECTORS = Sets.newHashSet("Financials");

    private static final Period[] MEMBER_OPTION_PERIODS = new Period[] { Period.ofMonths(3), Period.ofMonths(6) };

    private BigDecimal _numContracts;
    private int _numOptions;
    private int _numMembers;

    /**
     * In units of currency
     */
    private static final BigDecimal VALUE_OF_UNDERLYING = BigDecimal.valueOf(100000);

    /**
     * The default genearted portfolio name.
     */
    public static final String PORTFOLIO_NAME = "Equity Option Portfolio";

    private static Map<String, String> getIndexToExchangeMap() {
        final Map<String, String> ret = new HashMap<String, String>();
        ret.put("SPX", "US"); //S&P 500 -> combined US
        // ret.put("IBX", "BZ"); //Sao Paulo Stock Exchange IBrX Index -> combined Brazil
        //ret.put("TW50", "TT"); // FTSE TWSE Taiwan 50 Indx -> Taiwan Stock Exchange
        return ret;
    }

    //-------------------------------------------------------------------------
    /**
     * Main method to run the tool. No arguments are needed.
     *
     * @param args the arguments, unused
     */
    public static void main(final String[] args) { // CSIGNORE
        final boolean success = new DemoEquityOptionCollarPortfolioLoader().initAndRun(args,
                IntegrationToolContext.class);
        System.exit(success ? 0 : 1);
    }

    //-------------------------------------------------------------------------
    protected ManageablePortfolio generatePortfolio(final String portfolioName) {
        final ReferenceDataProvider referenceDataProvider = getToolContext().getBloombergReferenceDataProvider();

        final ManageablePortfolio portfolio = new ManageablePortfolio(portfolioName);

        //Is this a hack?
        final ManageablePortfolioNode rootNode = portfolio.getRootNode();
        portfolio.setRootNode(rootNode);

        //    String indexTickerSuffix = " Index";

        final Set<String> memberEquities = new HashSet<String>();

        for (final Entry<String, String> entry : INDEXES_TO_EXCHANGE.entrySet()) {

            final String indexTickerSuffix = " Index";

            final String underlying = entry.getKey();
            final String ticker = underlying + indexTickerSuffix;

            //don't add index (delete at some point)
            //      addNodes(rootNode, ticker, false, INDEX_OPTION_PERIODS);

            final Set<String> indexMembers = BloombergDataUtils.getIndexMembers(referenceDataProvider, ticker);
            for (final String member : indexMembers) {
                final String symbol = getBloombergEquitySymbol(entry.getValue(), member);
                //time series errors for Walmart
                //Todo: investegate & fix
                if ("WMT US Equity".equals(symbol)) {
                    continue;
                }
                memberEquities.add(symbol);
            }
        }

        // Sort the symbols for the current index by market cap (highest to lowest), skipping any in the list of EXCLUDED_SECTORS
        final TreeMap<Double, String> equityByMarketCap = new TreeMap<Double, String>();
        final Map<String, FudgeMsg> refDataMap = referenceDataProvider.getReferenceData(memberEquities, Sets
                .newHashSet(BloombergFields.CURRENT_MARKET_CAP_FIELD, BloombergConstants.FIELD_GICS_SUB_INDUSTRY));
        for (final String equity : memberEquities) {
            final FudgeMsg fieldData = refDataMap.get(equity);
            if (fieldData == null) {
                throw new OpenGammaRuntimeException("Information not found for equity: " + equity);
            }
            final String gicsCodeString = fieldData.getString(BloombergConstants.FIELD_GICS_SUB_INDUSTRY);
            if (gicsCodeString == null) {
                continue;
            }
            final GICSCode gicsCode = GICSCode.of(gicsCodeString);
            if (EXCLUDED_SECTORS.contains(gicsCode.getSectorDescription())) {
                continue;
            }
            final Double marketCap = fieldData.getDouble(BloombergFields.CURRENT_MARKET_CAP_FIELD);
            if (marketCap != null) {
                equityByMarketCap.put(marketCap, equity);
            }
        }

        // Add a given number of symbols (MEMBERS_DEPTH) to the portfolio and store in a List
        // When adding to the portfolio, add a collar of options with PVs distributed equally +/- around 0
        int count = 0;
        final List<String> chosenEquities = new ArrayList<String>();
        for (final Entry<Double, String> entry : equityByMarketCap.descendingMap().entrySet()) {
            try {
                addNodes(rootNode, entry.getValue(), true, MEMBER_OPTION_PERIODS);
                chosenEquities.add(entry.getValue());
                if (++count >= _numMembers) {
                    break;
                }
            } catch (final RuntimeException e) {
                s_logger.warn("Caught exception", e);
            }
        }
        s_logger.info("Generated collar portfolio for {}", chosenEquities);
        return portfolio;
    }

    private String getBloombergEquitySymbol(final String base, final String member) {
        return member.split(" ")[0] + " " + base + BLOOMBERG_EQUITY_TICKER_SUFFIX;
    }

    private void addNodes(final ManageablePortfolioNode rootNode, final String underlying,
            final boolean includeUnderlying, final Period[] expiries) {
        final ExternalId ticker = ExternalSchemes.bloombergTickerSecurityId(underlying);
        ManageableSecurity underlyingSecurity = null;
        if (includeUnderlying) {
            underlyingSecurity = getOrLoadEquity(ticker);
        }

        final ExternalIdBundle bundle = underlyingSecurity == null ? ExternalIdBundle.of(ticker)
                : underlyingSecurity.getExternalIdBundle();
        final HistoricalTimeSeriesInfoDocument timeSeriesInfo = getOrLoadTimeSeries(ticker, bundle);
        final double estimatedCurrentStrike = getOrLoadMostRecentPoint(timeSeriesInfo);
        final Set<ExternalId> optionChain = getOptionChain(ticker);

        //TODO: reuse positions/nodes?
        final String longName = underlyingSecurity == null ? "" : underlyingSecurity.getName();
        final String formattedName = MessageFormatter.format("[{}] {}", underlying, longName).getMessage();
        final ManageablePortfolioNode equityNode = new ManageablePortfolioNode(formattedName);

        final BigDecimal underlyingAmount = VALUE_OF_UNDERLYING.divide(BigDecimal.valueOf(estimatedCurrentStrike),
                BigDecimal.ROUND_HALF_EVEN);

        if (includeUnderlying) {
            addPosition(equityNode, underlyingAmount, ticker);
        }

        final TreeMap<LocalDate, Set<BloombergTickerParserEQOption>> optionsByExpiry = new TreeMap<LocalDate, Set<BloombergTickerParserEQOption>>();
        for (final ExternalId optionTicker : optionChain) {
            s_logger.debug("Got option {}", optionTicker);

            final BloombergTickerParserEQOption optionInfo = BloombergTickerParserEQOption
                    .getOptionParser(optionTicker);
            s_logger.debug("Got option info {}", optionInfo);

            final LocalDate key = optionInfo.getExpiry();
            Set<BloombergTickerParserEQOption> set = optionsByExpiry.get(key);
            if (set == null) {
                set = new HashSet<BloombergTickerParserEQOption>();
                optionsByExpiry.put(key, set);
            }
            set.add(optionInfo);
        }
        final Set<ExternalId> tickersToLoad = new HashSet<ExternalId>();

        final BigDecimal expiryCount = BigDecimal.valueOf(expiries.length);
        final BigDecimal defaultAmountAtExpiry = underlyingAmount.divide(expiryCount, BigDecimal.ROUND_DOWN);
        final BigDecimal spareAmountAtExpiry = defaultAmountAtExpiry.add(BigDecimal.ONE);
        int spareCount = underlyingAmount.subtract(defaultAmountAtExpiry.multiply(expiryCount)).intValue();

        for (final Period bucketPeriod : expiries) {
            final ManageablePortfolioNode bucketNode = new ManageablePortfolioNode(
                    bucketPeriod.toString().substring(1));

            final LocalDate nowish = LocalDate.now().withDayOfMonth(20); //This avoids us picking different options every time this script is run
            final LocalDate targetExpiry = nowish.plus(bucketPeriod);
            final LocalDate chosenExpiry = optionsByExpiry.floorKey(targetExpiry);
            if (chosenExpiry == null) {
                s_logger.info("No options for {} on {}", targetExpiry, underlying);
                continue;
            }
            s_logger.info("Using time {} for bucket {} ({})",
                    new Object[] { chosenExpiry, bucketPeriod, targetExpiry });

            final Set<BloombergTickerParserEQOption> optionsAtExpiry = optionsByExpiry.get(chosenExpiry);
            final TreeMap<Double, Pair<BloombergTickerParserEQOption, BloombergTickerParserEQOption>> optionsByStrike = new TreeMap<>();
            for (final BloombergTickerParserEQOption option : optionsAtExpiry) {
                //        s_logger.info("option {}", option);
                final double key = option.getStrike();
                Pair<BloombergTickerParserEQOption, BloombergTickerParserEQOption> pair = optionsByStrike.get(key);
                if (pair == null) {
                    pair = Pair.of(null, null);
                }
                if (option.getOptionType() == OptionType.CALL) {
                    pair = Pair.of(option, pair.getSecond());
                } else {
                    pair = Pair.of(pair.getFirst(), option);
                }
                optionsByStrike.put(key, pair);
            }

            //cascading collar?
            final BigDecimal amountAtExpiry = spareCount-- > 0 ? spareAmountAtExpiry : defaultAmountAtExpiry;

            s_logger.info(" est strike {}", estimatedCurrentStrike);
            final Double[] strikes = optionsByStrike.keySet().toArray(new Double[0]);

            int strikeIndex = Arrays.binarySearch(strikes, estimatedCurrentStrike);
            if (strikeIndex < 0) {
                strikeIndex = -(1 + strikeIndex);
            }
            s_logger.info("strikes length {} index {} strike of index {}",
                    new Object[] { Integer.valueOf(strikes.length), Integer.valueOf(strikeIndex),
                            Double.valueOf(strikes[strikeIndex]) });

            int minIndex = strikeIndex - _numOptions;
            minIndex = Math.max(0, minIndex);
            int maxIndex = strikeIndex + _numOptions;
            maxIndex = Math.min(strikes.length - 1, maxIndex);

            s_logger.info("min {} max {}", Integer.valueOf(minIndex), Integer.valueOf(maxIndex));
            final StringBuffer sb = new StringBuffer("strikes: [");
            for (int j = minIndex; j <= maxIndex; j++) {
                sb.append(" ");
                sb.append(strikes[j]);
            }
            sb.append(" ]");
            s_logger.info(sb.toString());

            //Short Calls
            final ArrayList<Pair<BloombergTickerParserEQOption, BloombergTickerParserEQOption>> calls = new ArrayList<Pair<BloombergTickerParserEQOption, BloombergTickerParserEQOption>>();
            for (int j = minIndex; j < strikeIndex; j++) {
                final Pair<BloombergTickerParserEQOption, BloombergTickerParserEQOption> pair = optionsByStrike
                        .get(strikes[j]);
                if (pair == null) {
                    throw new OpenGammaRuntimeException("no pair for strike" + strikes[j]);
                }
                calls.add(pair);
            }
            spreadOptions(bucketNode, calls, OptionType.CALL, -1, tickersToLoad, amountAtExpiry, includeUnderlying,
                    calls.size());

            // Long Puts
            final ArrayList<Pair<BloombergTickerParserEQOption, BloombergTickerParserEQOption>> puts = new ArrayList<Pair<BloombergTickerParserEQOption, BloombergTickerParserEQOption>>();
            for (int j = strikeIndex + 1; j <= maxIndex; j++) {
                final Pair<BloombergTickerParserEQOption, BloombergTickerParserEQOption> pair = optionsByStrike
                        .get(strikes[j]);
                if (pair == null) {
                    throw new OpenGammaRuntimeException("no pair for strike" + strikes[j]);
                }
                puts.add(pair);
            }
            spreadOptions(bucketNode, puts, OptionType.PUT, 1, tickersToLoad, amountAtExpiry, includeUnderlying,
                    puts.size());

            if (bucketNode.getChildNodes().size() + bucketNode.getPositionIds().size() > 0) {
                equityNode.addChildNode(bucketNode); //Avoid generating empty nodes
            }
        }

        for (final ExternalId optionTicker : tickersToLoad) {
            final ManageableSecurity loaded = getOrLoadSecurity(optionTicker);
            if (loaded == null) {
                throw new OpenGammaRuntimeException("Unexpected option type " + loaded);
            }

            //TODO [LAPANA-29] Should be able to do this for index options too
            if (includeUnderlying) {
                try {
                    final HistoricalTimeSeriesInfoDocument loadedTs = getOrLoadTimeSeries(optionTicker,
                            loaded.getExternalIdBundle());
                    if (loadedTs == null) {
                        throw new OpenGammaRuntimeException("Failed to get time series for " + loaded);
                    }
                } catch (final Exception ex) {
                    s_logger.info("Failed to get time series for " + loaded, ex);
                }
            }
        }

        if (equityNode.getPositionIds().size() + equityNode.getChildNodes().size() > 0) {
            rootNode.addChildNode(equityNode);
        }
    }

    private void spreadOptions(final ManageablePortfolioNode bucketNode,
            final Collection<Pair<BloombergTickerParserEQOption, BloombergTickerParserEQOption>> options,
            final OptionType type, final int scale, final Set<ExternalId> tickersToLoad,
            final BigDecimal underlyingAmount, final boolean includeUnderlying, final int targetNumber) {

        if (targetNumber == 0) {
            return;
        }

        final Collection<BloombergTickerParserEQOption> chosen = new ArrayList<BloombergTickerParserEQOption>();

        int remaining = targetNumber;
        for (final Pair<BloombergTickerParserEQOption, BloombergTickerParserEQOption> pair : options) {
            BloombergTickerParserEQOption option;
            if (type == OptionType.PUT) {
                option = pair.getSecond();
            } else {
                option = pair.getFirst();
            }

            //TODO [LAPANA-29] Should be able to do this for index options too
            if (includeUnderlying) {
                try {
                    final HistoricalTimeSeriesInfoDocument loadedTs = getOrLoadTimeSeries(option.getIdentifier());
                    final HistoricalTimeSeries ts = getToolContext().getHistoricalTimeSeriesSource()
                            .getHistoricalTimeSeries(loadedTs.getUniqueId(), LocalDate.now().minusWeeks(1), true,
                                    LocalDate.now(), true);
                    if (ts.getTimeSeries().isEmpty()) {
                        s_logger.info("No recent time series points for " + option.getIdentifier());
                        //   leave in for now
                        //          continue; //This option is not liquid enough for us
                    }
                } catch (final Exception ex) {
                    s_logger.info("Failed to get time series for " + option.getIdentifier(), ex);
                    //TODO: stop refetching this series each time
                    continue; //This option is not liquid enough for us
                }
            }
            chosen.add(option);
            remaining--;
            if (remaining == 0) {
                break;
            }
        }

        if (chosen.size() == 0) {
            s_logger.warn("Couldn't find any liquid " + type + " options from " + options);
            return; //TODO: should we try another expiry?
        }
        for (final BloombergTickerParserEQOption option : chosen) {
            tickersToLoad.add(option.getIdentifier());
            addPosition(bucketNode, BigDecimal.valueOf(scale).multiply(_numContracts), option.getIdentifier());
        }
    }

    private void addPosition(final ManageablePortfolioNode node, final BigDecimal amount,
            final ExternalId optionTicker) {
        final ManageablePosition position = new ManageablePosition(amount, optionTicker);

        final LocalDate tradeDate = getRandomTradeDate(optionTicker);
        final ManageableTrade trade = new ManageableTrade(amount, optionTicker, tradeDate, null,
                ExternalId.of("CPARTY", "BACS"));

        position.addTrade(trade);
        final PositionDocument doc = new PositionDocument(position);
        final PositionDocument added = getToolContext().getPositionMaster().add(doc);
        node.addPosition(added);
    }

    private LocalDate getRandomTradeDate(final ExternalId ticker) {
        final int tradeAge = (int) (3 + (Math.random() * 30));
        final LocalDate tradeDate = LocalDate.now().minusDays(tradeAge);
        //TODO: pick a date for which PX_LAST is known
        return tradeDate;
    }

    private Set<ExternalId> getOptionChain(final ExternalId ticker) {
        if (ticker.getScheme() != ExternalSchemes.BLOOMBERG_TICKER) {
            throw new OpenGammaRuntimeException("Not a bloomberg ticker " + ticker);
        }
        final ReferenceDataProvider referenceDataProvider = getToolContext().getBloombergReferenceDataProvider();

        final Set<ExternalId> optionChain = BloombergDataUtils.getOptionChain(referenceDataProvider,
                ticker.getValue()); //TODO [BBG-88] this query shouldn't get cached permanently
        if (optionChain == null) {
            throw new OpenGammaRuntimeException("Failed to get option chain for " + ticker);
        }
        s_logger.info("Got option chain {}", optionChain);
        return optionChain;
    }

    private double getOrLoadMostRecentPoint(HistoricalTimeSeriesInfoDocument timeSeriesInfo) {
        HistoricalTimeSeries timeSeries = getAllowedRecentPoints(timeSeriesInfo);
        if (timeSeries == null || timeSeries.getTimeSeries().isEmpty()) {
            if (timeSeries == null) {
                timeSeriesInfo = loadTimeSeries(timeSeriesInfo);
            } else if (timeSeries.getTimeSeries().isEmpty()) {
                timeSeries = updateTimeSeries(timeSeries);
            }
            timeSeries = getAllowedRecentPoints(timeSeriesInfo);
            if (timeSeries == null || timeSeries.getTimeSeries().isEmpty()) {
                throw new OpenGammaRuntimeException("Couldn't load recent points for " + timeSeriesInfo);
            }
        }
        final Double latestValue = timeSeries.getTimeSeries().getLatestValue();
        if (latestValue == null) {
            throw new OpenGammaRuntimeException("Unexpected null latest vaule");
        }
        return latestValue;
    }

    private HistoricalTimeSeries getAllowedRecentPoints(final HistoricalTimeSeriesInfoDocument timeSeriesInfo) {
        final LocalDate from = oldestTimeSeriesAllowed();
        final HistoricalTimeSeries timeSeries = getToolContext().getHistoricalTimeSeriesSource()
                .getHistoricalTimeSeries(timeSeriesInfo.getUniqueId().toLatest(), from, true, LocalDate.now(),
                        true);
        return timeSeries;
    }

    private HistoricalTimeSeriesInfoDocument getOrLoadTimeSeries(final ExternalId ticker) {
        return getOrLoadTimeSeries(ticker, ExternalIdBundle.of(ticker));
    }

    private HistoricalTimeSeriesInfoDocument getOrLoadTimeSeries(final ExternalId ticker,
            final ExternalIdBundle idBundle) {

        final ExternalIdBundle searchBundle = idBundle.withoutScheme(ExternalSchemes.ISIN); //For things which move country, e.g. ISIN(VALE5 BZ Equity) == ISIN(RIODF US Equity)
        final HistoricalTimeSeriesInfoSearchRequest htsRequest = new HistoricalTimeSeriesInfoSearchRequest(
                searchBundle);
        htsRequest.setDataField("PX_LAST");
        final HistoricalTimeSeriesInfoSearchResult htsSearch = getToolContext().getHistoricalTimeSeriesMaster()
                .search(htsRequest);
        switch (htsSearch.getDocuments().size()) {
        case 0:
            return loadTimeSeries(idBundle);
        case 1:
            break;
        default:
            throw new OpenGammaRuntimeException("Multiple time series match " + htsSearch);
        }
        final HistoricalTimeSeriesInfoDocument timeSeriesInfo = htsSearch.getDocuments().get(0);
        s_logger.debug("Loaded time series info {} for underlying {}", timeSeriesInfo, ticker);
        return timeSeriesInfo;
    }

    private HistoricalTimeSeriesInfoDocument loadTimeSeries(ExternalIdBundle idBundle) {
        final ReferenceDataProvider referenceDataProvider = getToolContext().getBloombergReferenceDataProvider();
        if (idBundle.getExternalId(ExternalSchemes.BLOOMBERG_BUID) == null
                && idBundle.getExternalId(ExternalSchemes.BLOOMBERG_TICKER) != null) {
            //For some reason loading some series by TICKER fails, but BUID works
            final BiMap<String, ExternalIdBundle> map = BloombergDataUtils
                    .convertToBloombergBuidKeys(Collections.singleton(idBundle), referenceDataProvider);
            if (map.size() != 1) {
                throw new OpenGammaRuntimeException("Failed to get buid");
            }
            for (final String key : map.keySet()) {
                final String buid = referenceDataProvider.getReferenceDataValue(key,
                        BloombergConstants.FIELD_ID_BBG_UNIQUE);
                idBundle = idBundle.withExternalId(ExternalSchemes.bloombergBuidSecurityId(buid));
            }
        }
        final ExternalIdBundle searchBundle = idBundle.withoutScheme(ExternalSchemes.ISIN); // For things which move country, e.g. ISIN(VALE5 BZ Equity) == ISIN(RIODF US Equity)
        final Map<ExternalId, UniqueId> timeSeries = getToolContext().getHistoricalTimeSeriesLoader()
                .loadTimeSeries(searchBundle.getExternalIds(), "UNKNOWN", "PX_LAST", LocalDate.now().minusYears(1),
                        null);
        if (timeSeries.size() != 1) {
            throw new OpenGammaRuntimeException("Failed to load time series " + idBundle + " " + timeSeries);
        }
        for (final UniqueId uid : timeSeries.values()) {
            return getToolContext().getHistoricalTimeSeriesMaster().get(uid);
        }
        throw new OpenGammaRuntimeException("Unexpected state");
    }

    private HistoricalTimeSeries updateTimeSeries(final HistoricalTimeSeries timeSeries) {
        if (!getToolContext().getHistoricalTimeSeriesLoader().updateTimeSeries(timeSeries.getUniqueId())) {
            throw new OpenGammaRuntimeException("Failed to update time series " + timeSeries);
        }
        //Force a cache miss on the source
        final HistoricalTimeSeriesInfoDocument newUid = getToolContext().getHistoricalTimeSeriesMaster()
                .get(timeSeries.getUniqueId().toLatest());
        return getToolContext().getHistoricalTimeSeriesSource().getHistoricalTimeSeries(newUid.getUniqueId());
    }

    private HistoricalTimeSeriesInfoDocument loadTimeSeries(final HistoricalTimeSeriesInfoDocument timeSeriesInfo) {
        final ExternalIdBundle idBundle = timeSeriesInfo.getInfo().getExternalIdBundle().toBundle(LocalDate.now());
        return loadTimeSeries(idBundle);
    }

    private LocalDate oldestTimeSeriesAllowed() {
        return LocalDate.now().minusWeeks(1);
    }

    private ManageableSecurity getOrLoadEquity(final ExternalId ticker) {
        final ManageableSecurity underlyingSecurity = getOrLoadSecurity(ticker);
        if (!EquitySecurity.SECURITY_TYPE.equals(underlyingSecurity.getSecurityType())) {
            throw new OpenGammaRuntimeException("Underlying is not an equity");
        }
        return underlyingSecurity;
    }

    private ManageableSecurity getOrLoadSecurity(final ExternalId ticker) {
        final SecuritySearchResult underlyingSearch = getToolContext().getSecurityMaster()
                .search(new SecuritySearchRequest(ticker));
        switch (underlyingSearch.getDocuments().size()) {
        case 0:
            s_logger.debug("Loading security for underlying {}", ticker);
            return loadSecurity(ticker);
        case 1:
            return underlyingSearch.getSingleSecurity();
        default:
            // Duplicate securities in the master
            s_logger.info("Multiple securities matched search for ticker {}. Using the first. {}", ticker,
                    underlyingSearch);
            return underlyingSearch.getFirstSecurity();
        }
    }

    private ManageableSecurity loadSecurity(final ExternalId ticker) {
        final ExternalIdBundle tickerBundle = ExternalIdBundle.of(ticker);
        final Collection<ExternalIdBundle> bundles = Collections.singleton(tickerBundle);
        final Map<ExternalIdBundle, UniqueId> loaded = getToolContext().getSecurityLoader().loadSecurities(bundles);
        final UniqueId loadedSec = loaded.get(tickerBundle);
        if (loadedSec == null) {
            throw new OpenGammaRuntimeException("Failed to load security for " + ticker);
        }
        return getToolContext().getSecurityMaster().get(loadedSec).getSecurity();
    }

    /**
     * Stores the portfolio.
     *
     * @param portfolio the portfolio, not null
     */
    private void storePortfolio(final ManageablePortfolio portfolio) {
        final PortfolioMaster portfolioMaster = getToolContext().getPortfolioMaster();

        final PortfolioSearchRequest req = new PortfolioSearchRequest();
        req.setName(portfolio.getName());
        final PortfolioSearchResult result = portfolioMaster.search(req);
        switch (result.getDocuments().size()) {
        case 0:
            s_logger.info("Creating new portfolio");
            portfolioMaster.add(new PortfolioDocument(portfolio));
            break;
        case 1:
            final UniqueId previousId = result.getDocuments().get(0).getUniqueId();
            s_logger.info("Updating portfolio {}", previousId);
            portfolio.setUniqueId(previousId);
            final PortfolioDocument document = new PortfolioDocument(portfolio);
            document.setUniqueId(previousId);
            portfolioMaster.update(document);
            break;
        default:
            throw new OpenGammaRuntimeException("Multiple portfolios matching " + req);
        }
    }

    @Override
    protected Options createOptions(final boolean mandatoryConfigResource) {
        final Options options = super.createOptions(mandatoryConfigResource);
        options.addOption(createPortfolioNameOption());
        options.addOption(createOptionDepthOption());
        options.addOption(createNumContractsOption());
        options.addOption(createNumMembersOption());
        return options;
    }

    private static Option createPortfolioNameOption() {
        OptionBuilder.withLongOpt("portfolio");
        OptionBuilder.withDescription("The name of the portfolio to create/update");
        OptionBuilder.hasArg();
        OptionBuilder.withArgName("resource");
        OptionBuilder.isRequired();
        return OptionBuilder.create(PORTFOLIO_NAME_OPT);
    }

    private static Option createOptionDepthOption() {
        OptionBuilder.withLongOpt("depth");
        OptionBuilder.withDescription("Number of options on either side of the strike price");
        OptionBuilder.hasArg();
        OptionBuilder.withArgName("resource");
        OptionBuilder.isRequired();
        return OptionBuilder.create(OPTION_DEPTH_OPT);
    }

    private static Option createNumContractsOption() {
        OptionBuilder.withLongOpt("contracts");
        OptionBuilder.withDescription("Number of contracts for each option");
        OptionBuilder.hasArg();
        OptionBuilder.withArgName("resource");
        OptionBuilder.isRequired();
        return OptionBuilder.create(NUM_CONTRACTS_OPT);
    }

    private static Option createNumMembersOption() {
        OptionBuilder.withLongOpt("members");
        OptionBuilder.withDescription("Number underlyers from index to include");
        OptionBuilder.hasArg();
        OptionBuilder.withArgName("resource");
        OptionBuilder.isRequired();
        return OptionBuilder.create(NUM_INDEX_MEMBERS_OPT);
    }

    @Override
    protected void doRun() throws Exception {
        s_logger.info(TOOL_NAME + " is initialising...");
        s_logger.info("Current working directory is " + System.getProperty("user.dir"));

        s_logger.info("Using portfolio \"{}\"", PORTFOLIO_NAME);
        s_logger.info("num index members: " + _numMembers);
        s_logger.info("Option Depth: " + _numOptions);
        s_logger.info("num contracts: {}", _numContracts.toString());
        final ManageablePortfolio portfolio = generatePortfolio(PORTFOLIO_NAME);
        storePortfolio(portfolio);

        s_logger.info(TOOL_NAME + " is finished.");
    }

    public void setNumContracts(final BigDecimal numContracts) {
        _numContracts = numContracts;
    }

    public void setNumOptions(final int numOptions) {
        _numOptions = numOptions;
    }

    public void setNumMembers(final int numMembers) {
        _numMembers = numMembers;
    }
}