com.opengamma.bbg.util.BloombergDataUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.opengamma.bbg.util.BloombergDataUtils.java

Source

/**
 * 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 com.opengamma.bbg.BloombergConstants.DATA_PROVIDER_UNKNOWN;
import static com.opengamma.bbg.BloombergConstants.DEFAULT_DATA_PROVIDER;
import static com.opengamma.bbg.BloombergConstants.FIELD_FUT_CHAIN;
import static com.opengamma.bbg.BloombergConstants.FIELD_ID_BBG_UNIQUE;
import static com.opengamma.bbg.BloombergConstants.FIELD_ID_CUSIP;
import static com.opengamma.bbg.BloombergConstants.FIELD_ID_ISIN;
import static com.opengamma.bbg.BloombergConstants.FIELD_ID_SEDOL1;
import static com.opengamma.bbg.BloombergConstants.FIELD_OPT_CHAIN;
import static com.opengamma.bbg.BloombergConstants.FIELD_PARSEKYABLE_DES;
import static com.opengamma.bbg.BloombergConstants.ON_OFF_FIELDS;
import static org.threeten.bp.temporal.ChronoField.DAY_OF_MONTH;
import static org.threeten.bp.temporal.ChronoField.MONTH_OF_YEAR;
import static org.threeten.bp.temporal.ChronoField.YEAR;
import static org.threeten.bp.temporal.ChronoUnit.MONTHS;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.text.DecimalFormat;
import java.util.ArrayList;
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.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.sf.ehcache.CacheManager;

import org.apache.commons.lang.StringUtils;
import org.fudgemsg.FudgeContext;
import org.fudgemsg.FudgeField;
import org.fudgemsg.FudgeMsg;
import org.fudgemsg.MutableFudgeMsg;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.threeten.bp.LocalDate;
import org.threeten.bp.Month;
import org.threeten.bp.OffsetTime;
import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;
import org.threeten.bp.format.DateTimeFormatter;
import org.threeten.bp.format.DateTimeFormatterBuilder;
import org.threeten.bp.format.DateTimeParseException;
import org.threeten.bp.temporal.TemporalAdjuster;

import com.bloomberglp.blpapi.Datetime;
import com.bloomberglp.blpapi.Element;
import com.bloomberglp.blpapi.Schema.Datatype;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.opengamma.OpenGammaRuntimeException;
import com.opengamma.analytics.math.matrix.DoubleMatrix1D;
import com.opengamma.bbg.BloombergConstants;
import com.opengamma.bbg.historical.normalization.BloombergFixedRateHistoricalTimeSeriesNormalizer;
import com.opengamma.bbg.historical.normalization.BloombergRateHistoricalTimeSeriesNormalizer;
import com.opengamma.bbg.livedata.normalization.BloombergRateRuleProvider;
import com.opengamma.bbg.normalization.BloombergRateClassifier;
import com.opengamma.bbg.referencedata.ReferenceData;
import com.opengamma.bbg.referencedata.ReferenceDataError;
import com.opengamma.bbg.referencedata.ReferenceDataProvider;
import com.opengamma.bbg.referencedata.ReferenceDataProviderGetRequest;
import com.opengamma.bbg.referencedata.ReferenceDataProviderGetResult;
import com.opengamma.core.historicaltimeseries.HistoricalTimeSeriesAdjuster;
import com.opengamma.core.historicaltimeseries.HistoricalTimeSeriesAdjustment;
import com.opengamma.core.historicaltimeseries.HistoricalTimeSeriesConstants;
import com.opengamma.core.id.ExternalSchemes;
import com.opengamma.core.value.MarketDataRequirementNames;
import com.opengamma.core.value.MarketDataRequirementNamesHelper;
import com.opengamma.engine.marketdata.availability.DomainMarketDataAvailabilityFilter;
import com.opengamma.engine.marketdata.availability.MarketDataAvailabilityFilter;
import com.opengamma.financial.analytics.ircurve.NextMonthlyExpiryAdjuster;
import com.opengamma.financial.security.option.OptionType;
import com.opengamma.id.ExternalId;
import com.opengamma.id.ExternalIdBundle;
import com.opengamma.id.ExternalIdBundleWithDates;
import com.opengamma.id.ExternalIdWithDates;
import com.opengamma.id.ExternalScheme;
import com.opengamma.livedata.normalization.FieldFilter;
import com.opengamma.livedata.normalization.FieldHistoryUpdater;
import com.opengamma.livedata.normalization.FieldNameChange;
import com.opengamma.livedata.normalization.ImpliedVolatilityCalculator;
import com.opengamma.livedata.normalization.MarketValueCalculator;
import com.opengamma.livedata.normalization.NormalizationRule;
import com.opengamma.livedata.normalization.NormalizationRuleSet;
import com.opengamma.livedata.normalization.RequiredFieldFilter;
import com.opengamma.livedata.normalization.SecurityRuleApplier;
import com.opengamma.livedata.normalization.SecurityRuleProvider;
import com.opengamma.livedata.normalization.StandardRules;
import com.opengamma.livedata.normalization.UnitChange;
import com.opengamma.master.historicaltimeseries.impl.HistoricalTimeSeriesFieldAdjustmentMap;
import com.opengamma.master.position.PositionDocument;
import com.opengamma.master.position.PositionMaster;
import com.opengamma.master.position.PositionSearchRequest;
import com.opengamma.master.position.impl.PositionSearchIterator;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.time.Expiry;
import com.opengamma.util.tuple.Pair;

/**
 * Utilities for working with data in the Bloomberg schema.
 * <p>
 * This is a thread-safe static utility class.
 */
public final class BloombergDataUtils {

    /** Logger. */
    private static final Logger s_logger = LoggerFactory.getLogger(BloombergDataUtils.class);

    private static final Pattern s_bloombergTickerPattern = buildPattern();

    private static final TemporalAdjuster s_monthlyExpiryAdjuster = new NextMonthlyExpiryAdjuster();

    /**
     * The standard fields required for Bloomberg data, as a list.
     */
    public static final List<String> STANDARD_FIELDS_LIST = ImmutableList.of("BID", "ASK", "LAST_PRICE",
            "PX_SETTLE", "VOLUME", "OPT_IMPLIED_VOLATILITY_BID_RT", "OPT_IMPLIED_VOLATILITY_ASK_RT",
            "OPT_IMPLIED_VOLATILITY_LAST_RT", "OPT_IMPLIED_VOLATILITY_MID_RT", "YLD_CNV_MID", //TODO BBG-96
            "YLD_YTM_MID", //TODO BBG-96
            "PX_DIRTY_MID", //TODO BBG-96
            "EQY_DVD_YLD_EST");

    /**
     * The standard fields required for Bloomberg data, as a set.
     */
    public static final Set<String> STANDARD_FIELDS_SET = new ImmutableSet.Builder<String>()
            .addAll(STANDARD_FIELDS_LIST).build();

    /**
     * Map from RIC to BBG prefixes, for exceptions only
     */
    private static final Map<String, String> s_ricToBbgPrefixMap;
    static {
        s_ricToBbgPrefixMap = new HashMap<String, String>();
        s_ricToBbgPrefixMap.put("EDD", "FD");
        s_ricToBbgPrefixMap.put("EDM", "0D");
        s_ricToBbgPrefixMap.put("EDD", "FD");
        s_ricToBbgPrefixMap.put("FEI", "ER");
        s_ricToBbgPrefixMap.put("FME", "0R");
        s_ricToBbgPrefixMap.put("2FME", "2R");
        s_ricToBbgPrefixMap.put("FSS", "L ");
        s_ricToBbgPrefixMap.put("FMS", "0L");
        s_ricToBbgPrefixMap.put("2FMS", "2L");
        s_ricToBbgPrefixMap.put("FES", "ES");
    }

    /**
     * Number format to use in BBC strike price
     */
    private static final DecimalFormat THREE_DECIMAL_PLACES = new DecimalFormat("#0.000");

    /**
     * Map from month to BBG month code
     */
    private static final BiMap<Month, String> s_monthCode;
    static {
        s_monthCode = HashBiMap.create();
        s_monthCode.put(Month.JANUARY, "F");
        s_monthCode.put(Month.FEBRUARY, "G");
        s_monthCode.put(Month.MARCH, "H");
        s_monthCode.put(Month.APRIL, "J");
        s_monthCode.put(Month.MAY, "K");
        s_monthCode.put(Month.JUNE, "M");
        s_monthCode.put(Month.JULY, "N");
        s_monthCode.put(Month.AUGUST, "Q");
        s_monthCode.put(Month.SEPTEMBER, "U");
        s_monthCode.put(Month.OCTOBER, "V");
        s_monthCode.put(Month.NOVEMBER, "X");
        s_monthCode.put(Month.DECEMBER, "Z");
    }

    /**
     * The observation time map.
     */
    private static final Map<String, String> s_observationTimeMap = ImmutableMap.<String, String>builder()
            .put("CMPL", HistoricalTimeSeriesConstants.LONDON_CLOSE)
            .put("CMPT", HistoricalTimeSeriesConstants.TOKYO_CLOSE)
            .put("CMPN", HistoricalTimeSeriesConstants.NEWYORK_CLOSE)
            .put(DEFAULT_DATA_PROVIDER, HistoricalTimeSeriesConstants.DEFAULT_OBSERVATION_TIME).build();

    /**
     * Restricted constructor.
     */
    private BloombergDataUtils() {
    }

    private static Pattern buildPattern() {
        if (BloombergConstants.MARKET_SECTORS.isEmpty()) {
            throw new OpenGammaRuntimeException("Bloomberg market sectors can not be empty");
        }
        final List<String> marketSectorList = Lists.newArrayList(BloombergConstants.MARKET_SECTORS);
        String sectorPattern = marketSectorList.get(0);
        for (int i = 1; i < marketSectorList.size(); i++) {
            sectorPattern += "|" + marketSectorList.get(i);
        }
        final Pattern bloombergTickerPattern = Pattern.compile(String.format("^(.+)(\\s+)((%s))$", sectorPattern),
                Pattern.CASE_INSENSITIVE);
        return bloombergTickerPattern;
    }

    public static Collection<NormalizationRuleSet> getDefaultNormalizationRules(
            final ReferenceDataProvider referenceDataProvider, final CacheManager cacheManager,
            ExternalScheme bbgScheme) {
        ArgumentChecker.notNull(cacheManager, "cacheManager");

        final Collection<NormalizationRuleSet> returnValue = new ArrayList<NormalizationRuleSet>();
        returnValue.add(StandardRules.getNoNormalization());

        final List<NormalizationRule> openGammaRules = new ArrayList<NormalizationRule>();

        // Filter out non-price updates
        openGammaRules.add(new FieldFilter(STANDARD_FIELDS_LIST));

        // Standardize field names.
        openGammaRules.add(new FieldNameChange("BID", MarketDataRequirementNames.BID));
        openGammaRules.add(new FieldNameChange("ASK", MarketDataRequirementNames.ASK));
        openGammaRules.add(new FieldNameChange("LAST_PRICE", MarketDataRequirementNames.LAST));
        openGammaRules.add(new FieldNameChange("PX_SETTLE", MarketDataRequirementNames.SETTLE_PRICE));
        openGammaRules.add(new FieldNameChange("VOLUME", MarketDataRequirementNames.VOLUME));
        openGammaRules.add(new FieldNameChange("OPT_IMPLIED_VOLATILITY_BID_RT",
                MarketDataRequirementNames.BID_IMPLIED_VOLATILITY));
        openGammaRules.add(new FieldNameChange("OPT_IMPLIED_VOLATILITY_ASK_RT",
                MarketDataRequirementNames.ASK_IMPLIED_VOLATILITY));
        openGammaRules.add(new FieldNameChange("OPT_IMPLIED_VOLATILITY_LAST_RT",
                MarketDataRequirementNames.LAST_IMPLIED_VOLATILITY));
        openGammaRules.add(new FieldNameChange("OPT_IMPLIED_VOLATILITY_MID_RT",
                MarketDataRequirementNames.MID_IMPLIED_VOLATILITY));
        openGammaRules.add(new FieldNameChange("YLD_CNV_MID", MarketDataRequirementNames.YIELD_CONVENTION_MID));
        openGammaRules
                .add(new FieldNameChange("YLD_YTM_MID", MarketDataRequirementNames.YIELD_YIELD_TO_MATURITY_MID));
        openGammaRules.add(new FieldNameChange("PX_DIRTY_MID", MarketDataRequirementNames.DIRTY_PRICE_MID));
        openGammaRules.add(new FieldNameChange("EQY_DVD_YLD_EST", MarketDataRequirementNames.DIVIDEND_YIELD));

        // Calculate market value
        openGammaRules.add(new MarketValueCalculator());

        // Normalize the market value
        if (referenceDataProvider != null) {
            final BloombergRateClassifier rateClassifier = new BloombergRateClassifier(referenceDataProvider,
                    cacheManager, bbgScheme);
            final SecurityRuleProvider quoteRuleProvider = new BloombergRateRuleProvider(rateClassifier);
            openGammaRules.add(new SecurityRuleApplier(quoteRuleProvider));
        }
        openGammaRules.add(new UnitChange(0.01, MarketDataRequirementNames.DIVIDEND_YIELD,
                MarketDataRequirementNames.YIELD_YIELD_TO_MATURITY_MID)); // returned as % from bbg

        // Calculate implied vol value
        openGammaRules.add(new ImpliedVolatilityCalculator());

        // At this point, BID, ASK, LAST, MARKET_VALUE, Volume and various Bloomberg implied vol fields are stored in the history.
        openGammaRules.add(new FieldHistoryUpdater());

        // Filter out non-OpenGamma fields (i.e., BID, ASK, various Bloomberg implied vol fields)
        openGammaRules.add(new FieldFilter(MarketDataRequirementNames.MARKET_VALUE,
                MarketDataRequirementNames.SETTLE_PRICE, MarketDataRequirementNames.VOLUME,
                MarketDataRequirementNames.IMPLIED_VOLATILITY, MarketDataRequirementNames.YIELD_CONVENTION_MID,
                MarketDataRequirementNames.YIELD_YIELD_TO_MATURITY_MID, MarketDataRequirementNames.DIRTY_PRICE_MID,
                MarketDataRequirementNames.DIVIDEND_YIELD));
        openGammaRules.add(new RequiredFieldFilter(MarketDataRequirementNames.MARKET_VALUE));

        final NormalizationRuleSet openGammaRuleSet = new NormalizationRuleSet(
                StandardRules.getOpenGammaRuleSetId(), "", openGammaRules);
        returnValue.add(openGammaRuleSet);

        return returnValue;
    }

    public static HistoricalTimeSeriesFieldAdjustmentMap createFieldAdjustmentMap(
            final ReferenceDataProvider referenceDataProvider, final CacheManager cacheManager) {
        final HistoricalTimeSeriesFieldAdjustmentMap fieldAdjustmentMap = new HistoricalTimeSeriesFieldAdjustmentMap(
                BloombergConstants.BLOOMBERG_DATA_SOURCE_NAME);
        final BloombergRateClassifier rateClassifier = new BloombergRateClassifier(referenceDataProvider,
                cacheManager, ExternalSchemes.BLOOMBERG_BUID);
        final HistoricalTimeSeriesAdjuster rateNormalizer = new BloombergRateHistoricalTimeSeriesNormalizer(
                rateClassifier);
        final BloombergFixedRateHistoricalTimeSeriesNormalizer div100 = new BloombergFixedRateHistoricalTimeSeriesNormalizer(
                new HistoricalTimeSeriesAdjustment.DivideBy(100.0));
        fieldAdjustmentMap.addFieldAdjustment(MarketDataRequirementNames.SETTLE_PRICE, null,
                BloombergConstants.BBG_FIELD_SETTLE_PRICE, rateNormalizer);
        fieldAdjustmentMap.addFieldAdjustment(MarketDataRequirementNames.MARKET_VALUE, null,
                BloombergConstants.BBG_FIELD_LAST_PRICE, rateNormalizer);
        fieldAdjustmentMap.addFieldAdjustment(BloombergConstants.BBG_FIELD_LAST_PRICE, null,
                BloombergConstants.BBG_FIELD_LAST_PRICE, rateNormalizer);
        fieldAdjustmentMap.addFieldAdjustment(MarketDataRequirementNames.VOLUME, null,
                BloombergConstants.BBG_FIELD_VOLUME, null);
        fieldAdjustmentMap.addFieldAdjustment(MarketDataRequirementNames.YIELD_YIELD_TO_MATURITY_MID, null,
                BloombergConstants.BBG_FIELD_YIELD_TO_MATURITY_MID, null);
        fieldAdjustmentMap.addFieldAdjustment(MarketDataRequirementNames.DIVIDEND_YIELD, null,
                BloombergConstants.BBG_FIELD_DIVIDEND_YIELD, div100);
        return fieldAdjustmentMap;
    }

    public static MarketDataAvailabilityFilter createAvailabilityFilter() {
        final Set<ExternalScheme> acceptableSchemes = ImmutableSet.of(ExternalSchemes.BLOOMBERG_BUID_WEAK,
                ExternalSchemes.BLOOMBERG_BUID, ExternalSchemes.BLOOMBERG_TICKER_WEAK,
                ExternalSchemes.BLOOMBERG_TICKER);
        final Collection<String> validMarketDataRequirementNames = MarketDataRequirementNamesHelper
                .constructValidRequirementNames();
        return new DomainMarketDataAvailabilityFilter(acceptableSchemes, validMarketDataRequirementNames);
    }

    public static FudgeMsg parseElement(final Element element) {
        final MutableFudgeMsg fieldData = FudgeContext.GLOBAL_DEFAULT.newMessage();
        for (int iSubElement = 0; iSubElement < element.numElements(); iSubElement++) {
            final Element subElement = element.getElement(iSubElement);
            if (subElement.numValues() == 0) {
                continue;
            }
            final String name = subElement.elementDefinition().name().toString();
            final Object value = parseValue(subElement);
            if (value instanceof List<?>) {
                for (final Object obj : (List<?>) value) {
                    fieldData.add(name, obj);
                }
            } else if (value != null) {
                fieldData.add(name, value);
            } else {
                s_logger.warn("Unable to extract value named {} from element {}", name, subElement);
            }
        }
        return fieldData;
    }

    /**
     * @param valueElement the value element
     * @return the parsed value
     */
    public static Object parseValue(final Element valueElement) {
        final Datatype datatype = valueElement.datatype();
        if (datatype == Datatype.STRING) {
            return valueElement.getValueAsString();
        } else if (datatype == Datatype.BOOL) {
            return valueElement.getValueAsBool();
        } else if (datatype == Datatype.BYTEARRAY) {
            // REVIEW kirk 2009-10-22 -- How do we extract this? Intentionally fall through.
        } else if (datatype == Datatype.CHAR) {
            final char c = valueElement.getValueAsChar();
            return new String("" + c);
        } else if (datatype == Datatype.CHOICE) {
            // REVIEW kirk 2009-10-22 -- How do we extract this? Intentionally fall through.
        } else if (datatype == Datatype.DATE) {
            final Datetime date = valueElement.getValueAsDate();
            return date.toString();
        } else if (datatype == Datatype.DATETIME) {
            // REVIEW kirk 2009-10-22 -- This is clearly wrong.
            final Datetime date = valueElement.getValueAsDatetime();
            return date.toString();
            //return date.calendar().getTime();
        } else if (datatype == Datatype.ENUMERATION) {
            return valueElement.getValueAsString();
        } else if (datatype == Datatype.FLOAT32) {
            return valueElement.getValueAsFloat32();
        } else if (datatype == Datatype.FLOAT64) {
            return valueElement.getValueAsFloat64();
        } else if (datatype == Datatype.INT32) {
            return valueElement.getValueAsInt32();
        } else if (datatype == Datatype.INT64) {
            return valueElement.getValueAsInt64();
        } else if (datatype == Datatype.TIME) {
            // REVIEW kirk 2009-10-22 -- This is clearly wrong.
            final Datetime date = valueElement.getValueAsDate();
            return date.toString();
            //return date.calendar().getTime();
        } else if (datatype == Datatype.SEQUENCE) {
            final int numValues = valueElement.numValues();
            final List<FudgeMsg> valueAsList = new ArrayList<FudgeMsg>(numValues);
            for (int i = 0; i < numValues; i++) {
                final Element sequenceElem = valueElement.getValueAsElement(i);
                final FudgeMsg sequenceElemAsMsg = parseElement(sequenceElem);
                valueAsList.add(sequenceElemAsMsg);
            }
            return valueAsList;
        }
        s_logger.warn("Unhandled Datatype of {}, data {}", datatype, valueElement);
        return null;
    }

    public static String getSingleBUID(final ReferenceDataProvider refDataProvider, final String securityDes) {
        final Map<String, String> ticker2buid = getBUID(refDataProvider, Collections.singleton(securityDes));
        return ticker2buid.get(securityDes);
    }

    public static Map<String, String> getBUID(final ReferenceDataProvider refDataProvider,
            final Set<String> bloombergKeys) {
        return refDataProvider.getReferenceDataValues(bloombergKeys, FIELD_ID_BBG_UNIQUE);
    }

    public static Set<String> getIndexMembers(final ReferenceDataProvider refDataProvider,
            final String indexTicker) {
        final Set<String> result = new TreeSet<String>();
        final Set<String> bbgFields = new HashSet<String>();
        bbgFields.add("INDX_MEMBERS");
        bbgFields.add("INDX_MEMBERS2");
        bbgFields.add("INDX_MEMBERS3");
        final ReferenceDataProviderGetRequest dataRequest = ReferenceDataProviderGetRequest.createGet(indexTicker,
                bbgFields, true);
        final ReferenceDataProviderGetResult dataResult = refDataProvider.getReferenceData(dataRequest);
        final ReferenceData perSecResult = dataResult.getReferenceData(indexTicker);
        if (perSecResult.isIdentifierError()) {
            final List<ReferenceDataError> errors = perSecResult.getErrors();
            if (!errors.isEmpty()) {
                s_logger.warn("Unable to lookup Index {} members because of exceptions {}", indexTicker,
                        errors.toString());
                throw new OpenGammaRuntimeException(
                        "Unable to lookup Index members because of exceptions " + errors.toString());
            }
        }
        addIndexMembers(result, perSecResult, "INDX_MEMBERS");
        addIndexMembers(result, perSecResult, "INDX_MEMBERS2");
        addIndexMembers(result, perSecResult, "INDX_MEMBERS3");
        return result;
    }

    /**
     * @param result
     * @param perSecResult
     * @return
     */
    private static void addIndexMembers(final Set<String> result, final ReferenceData perSecResult,
            final String fieldName) {
        final FudgeMsg fieldData = perSecResult.getFieldValues();
        final List<FudgeField> fields = fieldData.getAllByName(fieldName);
        for (final FudgeField fudgeField : fields) {
            final FudgeMsg msg = (FudgeMsg) fudgeField.getValue();
            final String memberTicker = msg.getString("Member Ticker and Exchange Code");
            result.add(memberTicker);
        }
    }

    public static Set<ExternalId> getOptionChain(final ReferenceDataProvider refDataProvider,
            final String securityID) {
        ArgumentChecker.notNull(securityID, "security name");
        final Set<ExternalId> result = new TreeSet<ExternalId>();
        final FudgeMsg fieldData = refDataProvider
                .getReferenceData(Collections.singleton(securityID), Collections.singleton(FIELD_OPT_CHAIN))
                .get(securityID);
        if (fieldData == null) {
            s_logger.info("Reference data for security {} cannot be null", securityID);
            return null;
        }
        for (final FudgeField field : fieldData.getAllByName(FIELD_OPT_CHAIN)) {
            final FudgeMsg chainContainer = (FudgeMsg) field.getValue();
            final String identifier = StringUtils.trimToNull(chainContainer.getString("Security Description"));
            if (identifier != null) {
                final ExternalId ticker = ExternalSchemes
                        .bloombergTickerSecurityId(BloombergDataUtils.removeDuplicateWhiteSpace(identifier, " "));
                result.add(ticker);
            }
        }
        return result;
    }

    /**
     * Get the future chain for a security. There may be futures on multiple exchanges - in general need to restrict to exchanges using the same currency. Equities: restrict to One Chicago futures with
     * a lead market maker (e.g. AAPL=G3 OC Equity)
     * 
     * @param refDataProvider the reference data provider
     * @param securityID the security
     * @return the (ordered)
     */
    public static Set<ExternalId> getFuturechain(final ReferenceDataProvider refDataProvider,
            final String securityID) {
        ArgumentChecker.notNull(securityID, "security name");
        final Set<ExternalId> result = new TreeSet<>();

        final FudgeMsg fieldData = refDataProvider
                .getReferenceData(Collections.singleton(securityID), Collections.singleton(FIELD_FUT_CHAIN))
                .get(securityID);
        if (fieldData == null) {
            s_logger.info("Reference data for security {} cannot be null", securityID);
            return null;
        }

        for (final FudgeField field : fieldData.getAllByName(FIELD_FUT_CHAIN)) {
            final FudgeMsg chainContainer = (FudgeMsg) field.getValue();
            final String identifier = StringUtils.trimToNull(chainContainer.getString("Security Description"));
            if (identifier != null) {
                if (identifier.endsWith("OC Equity") && identifier.startsWith(securityID.split("\\s+")[0] + "=")) { // equity
                    final ExternalId ticker = ExternalSchemes.bloombergTickerSecurityId(
                            BloombergDataUtils.removeDuplicateWhiteSpace(identifier, " "));
                    result.add(ticker);
                }
            }
        }
        return result;
    }

    /**
     * Checks if the specified field contains valid data.
     * 
     * @param name the field name, not null
     * @return true if the field is valid
     */
    public static boolean isValidField(final String name) {
        return ON_OFF_FIELDS.contains(name) || (StringUtils.isNotBlank(name) && !name.equalsIgnoreCase("N.A"));
    }

    public static ExternalIdBundleWithDates parseIdentifiers(final FudgeMsg fieldData,
            final String firstTradeDateField, final String lastTradeDateField) {
        ArgumentChecker.notNull(fieldData, "fieldData");
        final String bbgUnique = fieldData.getString(FIELD_ID_BBG_UNIQUE);
        final String cusip = fieldData.getString(FIELD_ID_CUSIP);
        final String isin = fieldData.getString(FIELD_ID_ISIN);
        final String sedol1 = fieldData.getString(FIELD_ID_SEDOL1);
        final String securityIdentifier = fieldData.getString(FIELD_PARSEKYABLE_DES);
        final String validFromStr = firstTradeDateField != null ? fieldData.getString(firstTradeDateField) : null;
        final String validToStr = lastTradeDateField != null ? fieldData.getString(lastTradeDateField) : null;

        final Set<ExternalIdWithDates> identifiers = new HashSet<ExternalIdWithDates>();
        if (isValidField(bbgUnique)) {
            final ExternalId buid = ExternalSchemes.bloombergBuidSecurityId(bbgUnique);
            identifiers.add(ExternalIdWithDates.of(buid, null, null));
        }
        if (isValidField(cusip)) {
            final ExternalId cusipId = ExternalSchemes.cusipSecurityId(cusip);
            identifiers.add(ExternalIdWithDates.of(cusipId, null, null));
        }
        if (isValidField(sedol1)) {
            final ExternalId sedol1Id = ExternalSchemes.sedol1SecurityId(sedol1);
            identifiers.add(ExternalIdWithDates.of(sedol1Id, null, null));
        }
        if (isValidField(isin)) {
            final ExternalId isinId = ExternalSchemes.isinSecurityId(isin);
            identifiers.add(ExternalIdWithDates.of(isinId, null, null));
        }
        if (isValidField(securityIdentifier)) {
            final ExternalId tickerId = ExternalSchemes.bloombergTickerSecurityId(securityIdentifier);
            LocalDate validFrom = null;
            if (isValidField(validFromStr)) {
                try {
                    validFrom = LocalDate.parse(validFromStr);
                } catch (final DateTimeParseException ex) {
                    s_logger.warn("valid from date not in yyyy-mm-dd format - {}", validFromStr);
                }
            }
            LocalDate validTo = null;
            if (isValidField(validToStr)) {
                try {
                    validTo = LocalDate.parse(validToStr);
                } catch (final DateTimeParseException ex) {
                    s_logger.warn("valid to date not in yyyy-mm-dd format - {}", validToStr);
                }
            }
            identifiers.add(ExternalIdWithDates.of(tickerId, validFrom, validTo));
        }
        return new ExternalIdBundleWithDates(identifiers);
    }

    /**
     * @param bundleWithDates the identifier bundle with dates
     * @return {@link ExternalIdBundleWithDates} with single and 2 digit year codes
     */
    public static ExternalIdBundleWithDates addTwoDigitYearCode(final ExternalIdBundleWithDates bundleWithDates) {
        ArgumentChecker.notNull(bundleWithDates, "bundleWithDates");
        final Set<ExternalIdWithDates> identifiers = new HashSet<ExternalIdWithDates>();
        for (final ExternalIdWithDates identifierWithDates : bundleWithDates) {
            final ExternalId identifier = identifierWithDates.toExternalId();
            final String identifierValue = identifier.getValue();
            if (identifierValue.contains(BloombergConstants.MARKET_SECTOR_COMDTY)
                    && identifierValue.indexOf(' ') == identifierValue.lastIndexOf(' ')) {
                //found a future code
                final int splitIndex = identifierValue.lastIndexOf(' ');
                final String secCode = identifierValue.substring(0, splitIndex);

                //get two year digit code
                final int length = secCode.length();
                String yearStr = secCode.substring(length - 2);
                try {
                    Integer.parseInt(yearStr);
                    identifiers.add(ExternalIdWithDates.of(identifierWithDates.toExternalId(),
                            identifierWithDates.getValidTo().plusDays(1), null));

                    //add single digit as well
                    final StringBuilder buf = new StringBuilder(secCode.substring(0, secCode.indexOf(yearStr)));
                    buf.append(yearStr.charAt(yearStr.length() - 1));
                    buf.append(identifierValue.substring(splitIndex));
                    final ExternalId singleYearId = ExternalSchemes.bloombergTickerSecurityId(buf.toString());
                    identifiers.add(ExternalIdWithDates.of(singleYearId, identifierWithDates.getValidFrom(),
                            identifierWithDates.getValidTo()));
                } catch (final NumberFormatException ex) {
                    // try the single digit
                    yearStr = secCode.substring(length - 1);
                    try {
                        Integer.parseInt(yearStr);
                        identifiers.add(identifierWithDates);
                        //add double digit as well
                        final LocalDate validTo = identifierWithDates.getValidTo();
                        final String endYear = String.valueOf(validTo.getYear());
                        final StringBuilder buf = new StringBuilder(secCode.substring(0, secCode.indexOf(yearStr)));
                        buf.append(endYear.substring(endYear.length() - 2));
                        buf.append(identifierValue.substring(splitIndex));
                        final ExternalId doubleDigitYearId = ExternalSchemes
                                .bloombergTickerSecurityId(buf.toString());
                        identifiers.add(ExternalIdWithDates.of(doubleDigitYearId,
                                identifierWithDates.getValidTo().plusDays(1), null));
                    } catch (final NumberFormatException ex2) {
                        s_logger.warn("cannot make out year code for {}", identifier);
                    }
                }
            } else {
                identifiers.add(identifierWithDates);
            }
        }
        return new ExternalIdBundleWithDates(identifiers);
    }

    /**
     * Given a position master, it pulls the current positions identifier bundles.
     * 
     * @param positionMaster the position master, not-null
     * @return a set of bundles of current positions
     */
    public static Set<ExternalIdBundle> getCurrentIdentifiers(final PositionMaster positionMaster) {
        ArgumentChecker.notNull(positionMaster, "positionMaster");
        final PositionSearchRequest searchRequest = new PositionSearchRequest();
        final Set<ExternalIdBundle> securities = new HashSet<ExternalIdBundle>();
        for (final PositionDocument doc : PositionSearchIterator.iterable(positionMaster, searchRequest)) {
            securities.add(doc.getPosition().getSecurityLink().getExternalId()); // TODO: doesn't work if linked by object id
        }
        return securities;
    }

    /**
     * Removes duplicate whitespace from the specified field.
     * 
     * @param field the field name, not null
     * @param replacement the replacement string, not null
     * @return the stripped field, not null
     */
    public static String removeDuplicateWhiteSpace(final String field, final String replacement) {
        if (field != null) {
            ArgumentChecker.notNull(field, "field");
            ArgumentChecker.notNull(replacement, "replacement");
            return field.replaceAll("\\s+", replacement);
        } else {
            return null;
        }
    }

    public static Set<ExternalId> identifierLoader(final Reader reader) {
        ArgumentChecker.notNull(reader, "reader");
        final Set<ExternalId> result = Sets.newHashSet();
        final BufferedReader inputReader = new BufferedReader(reader);
        try {
            String line;
            while ((line = inputReader.readLine()) != null) {
                if (StringUtils.isBlank(line) || line.charAt(0) == '#') {
                    continue;
                }
                result.add(ExternalId.parse(line));
            }
        } catch (final IOException ex) {
            throw new OpenGammaRuntimeException("cannot read from reader", ex);
        } finally {
            try {
                inputReader.close();
            } catch (final IOException ex) {
                s_logger.warn("cannot close reader ", ex);
            }
        }
        return result;
    }

    /**
     * Convert bundles to preferred bloomberg keys. Where possible these keys will be BUIDs [BBG-87]
     * 
     * @param identifiers the collection of bundles, not null
     * @param refDataProvider the ReferenceDataProvider to use to resolve bundles not containing BUIDs, not null
     * @return BiMap of bloomberg key (hopefully a buid key) to bundle, not null
     */
    public static BiMap<String, ExternalIdBundle> convertToBloombergBuidKeys(
            final Collection<ExternalIdBundle> identifiers, final ReferenceDataProvider refDataProvider) {
        ArgumentChecker.notNull(identifiers, "identifiers");
        ArgumentChecker.notNull(refDataProvider, "refDataProvider");

        final Set<String> nonBuids = new HashSet<String>();
        final BiMap<String, ExternalIdBundle> bundle2Bbgkey = HashBiMap.create();
        for (final ExternalIdBundle identifierBundle : identifiers) {
            final ExternalId preferredIdentifier = BloombergDomainIdentifierResolver
                    .resolvePreferredIdentifier(identifierBundle);
            final String bloombergKey = BloombergDomainIdentifierResolver.toBloombergKey(preferredIdentifier);
            if (bloombergKey == null) {
                s_logger.warn("bundle {} resolves to null bloomberg key", identifierBundle);
                continue;
            }
            //REVIEW simon : is it ok that we discard duplicates here
            bundle2Bbgkey.put(bloombergKey, identifierBundle);
            if (!preferredIdentifier.getScheme().equals(ExternalSchemes.BLOOMBERG_BUID)) {
                nonBuids.add(bloombergKey);
            }
        }

        if (!nonBuids.isEmpty()) {
            //BBG-87 Map everything to BUIDs
            final Map<String, String> remaps = getBUID(refDataProvider, nonBuids);
            for (final Entry<String, String> entry : remaps.entrySet()) {
                final String nonBuid = entry.getKey();
                final String buid = entry.getValue();
                final ExternalId buidExternalId = ExternalId.of(ExternalSchemes.BLOOMBERG_BUID, buid);
                final String buidKey = BloombergDomainIdentifierResolver.toBloombergKey(buidExternalId);
                changeKey(nonBuid, buidKey, bundle2Bbgkey);
            }
        }
        return bundle2Bbgkey;
    }

    private static <TKey, TValue> void changeKey(final TKey oldKey, final TKey newKey,
            final BiMap<TKey, TValue> map) {
        //REVIEW simon : is it ok that we discard duplicates here
        final TValue oldValue = map.remove(oldKey);
        map.put(newKey, oldValue);
    }

    /**
     * Splits a ticker at the market sector, returning a pair of the ticker excluding the market sector and the market sector itself.
     * 
     * @param ticker the ticker, not null
     * @return a pair of the ticker excluding the market sector, and the market sector, not null
     */
    public static Pair<String, String> splitTickerAtMarketSector(final String ticker) {
        ArgumentChecker.notNull(ticker, "ticker");
        final int splitIdx = ticker.lastIndexOf(' ');
        if (splitIdx > 0) {
            return Pair.of(ticker.substring(0, splitIdx), ticker.substring(splitIdx + 1));
        } else {
            return null;
        }
    }

    public static boolean isValidBloombergTicker(final String ticker) {
        ArgumentChecker.notNull(ticker, "ticker");
        final Matcher matcher = s_bloombergTickerPattern.matcher(ticker);
        if (matcher.matches()) {
            final String marketSector = matcher.group(3);
            s_logger.debug("market sector {} extracted from ticker {}", marketSector, ticker);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Generates an equity option ticker from details about the option.
     * 
     * @param underlyingTicker the ticker of the underlying equity, not null
     * @param expiry the option expiry, not null
     * @param optionType the option type, not null
     * @param strike the strike rate
     * @return the equity option ticker, not null
     */
    public static ExternalId generateEquityOptionTicker(final String underlyingTicker, final Expiry expiry,
            final OptionType optionType, final double strike) {
        ArgumentChecker.notNull(underlyingTicker, "underlyingTicker");
        ArgumentChecker.notNull(expiry, "expiry");
        ArgumentChecker.notNull(optionType, "optionType");
        Pair<String, String> tickerMarketSectorPair = splitTickerAtMarketSector(underlyingTicker);
        DateTimeFormatter expiryFormatter = DateTimeFormatter.ofPattern("MM/dd/yy");
        DecimalFormat strikeFormat = new DecimalFormat("0.###");
        String strikeString = strikeFormat.format(strike);
        StringBuilder sb = new StringBuilder();
        sb.append(tickerMarketSectorPair.getFirst()).append(' ')
                .append(expiry.getExpiry().toString(expiryFormatter)).append(' ')
                .append(optionType == OptionType.PUT ? 'P' : 'C').append(strikeString).append(' ')
                .append(tickerMarketSectorPair.getSecond());
        return ExternalId.of(ExternalSchemes.BLOOMBERG_TICKER, sb.toString());
    }

    public static String dateToBbgCode(final LocalDate date) {
        final String y = Integer.toString(date.getYear());
        return s_monthCode.get(date.getMonth()) + y.charAt(y.length() - 1);
    }

    public static ExternalId ricToBbgFuture(final String ric) {
        return ExternalId.of(ExternalSchemes.BLOOMBERG_TICKER,
                ricToBbgFuturePrefix(ric) + ric.substring(ric.length() - 2, ric.length()) + " Comdty");
    }

    public static ExternalId ricToBbgFutureOption(final String ric, final boolean isCall, final double strike,
            final LocalDate expiry) {
        final String result = ricToBbgFuturePrefix(ric) + dateToBbgCode(expiry) + (isCall ? "C" : "P") + " "
                + THREE_DECIMAL_PLACES.format(strike) + " Comdty";
        return ExternalId.of(ExternalSchemes.BLOOMBERG_TICKER, result);
    }

    private static String ricToBbgFuturePrefix(String ric) {
        if (ric.length() < 4) {
            return null;
        }
        ric = ric.trim().toUpperCase();
        final String front = ric.substring(0, (Character.isDigit(ric.charAt(0)) ? 4 : 3));
        if (s_ricToBbgPrefixMap.containsKey(front)) {
            return s_ricToBbgPrefixMap.get(front);
        } else {
            throw new OpenGammaRuntimeException("Could not map RIC onto BBG code");
        }
    }

    public static ExternalId futureBundleToGenericFutureTicker(ExternalIdBundle bundle, ZonedDateTime now,
            OffsetTime futureExpiryTime, ZoneId futureExpiryTimeZone) {
        ZonedDateTime nextExpiry = now.toLocalDate().with(s_monthlyExpiryAdjuster).atTime(now.toLocalTime())
                .atZone(now.getZone());
        ExternalId bbgTicker = bundle.getExternalId(ExternalSchemes.BLOOMBERG_TICKER);
        if (bbgTicker == null) {
            throw new OpenGammaRuntimeException(
                    "Could not find a Bloomberg Ticker in the supplied bundle " + bundle.toString());
        }
        final String code = bbgTicker.getValue();
        final String marketSector = splitTickerAtMarketSector(code).getSecond();
        try {
            String typeCode;
            String monthCode;
            int year;
            if (code.length() > 4 && code.charAt(4) == ' ') {
                // four letter futures code
                typeCode = code.substring(0, 2);
                monthCode = code.substring(2, 3);
                year = Integer.parseInt(code.substring(3, 4));

                final int thisYear = now.getYear();
                if ((thisYear % 10) > year) {
                    year = ((thisYear / 10) * 10) + 10 + year;
                } else if ((thisYear % 10) == year) {
                    // This code assumes that the code is for this year, so constructs a trial date using the year and month and adjusts it forward to the expiry
                    // note we're not taking into account exchange closing time here.
                    final Month month = s_monthCode.inverse().get(monthCode);
                    if (month == null) {
                        throw new OpenGammaRuntimeException("Invalid month code " + monthCode);
                    }
                    LocalDate nextExpiryIfThisYear = LocalDate.of((((thisYear / 10) * 10) + year), month, 1)
                            .with(s_monthlyExpiryAdjuster);
                    ZonedDateTime nextExpiryDateTimeIfThisYear = nextExpiryIfThisYear.atTime(futureExpiryTime)
                            .atZoneSimilarLocal(futureExpiryTimeZone);
                    if (now.isAfter(nextExpiryDateTimeIfThisYear)) {
                        year = ((thisYear / 10) * 10) + 10 + year;
                    } else {
                        year = ((thisYear / 10) * 10) + year;
                    }
                } else {
                    year = ((thisYear / 10) * 10) + year;
                }
            } else if (code.length() > 5 && code.charAt(5) == ' ') {
                // five letter futures code
                typeCode = code.substring(0, 2);
                monthCode = code.substring(2, 3);
                s_logger.warn("Parsing retired futures code format {}", code);
                year = Integer.parseInt(code.substring(3, 5));
                if (year > 70) { // 58 year time bomb and ticking...
                    year += 1900;
                } else {
                    year += 2000;
                }
            } else {
                s_logger.warn("Unknown futures code format {}", code);
                return null;
            }
            // phew.
            // now we generate the expiry of the future from the code:
            // Again, note that we're not taking into account exchange trading hours.
            LocalDate expiryDate = LocalDate.of(year, s_monthCode.inverse().get(monthCode), 1)
                    .with(s_monthlyExpiryAdjuster);
            ZonedDateTime expiry = expiryDate.atTime(futureExpiryTime).atZoneSimilarLocal(futureExpiryTimeZone);
            int quarters = (int) nextExpiry.periodUntil(expiry, MONTHS) / 3;
            int genericFutureNumber = quarters + 1;
            StringBuilder sb = new StringBuilder();
            sb.append(typeCode);
            sb.append(genericFutureNumber);
            sb.append(" ");
            sb.append(marketSector);
            return ExternalId.of(ExternalSchemes.BLOOMBERG_TICKER, sb.toString());
        } catch (final NumberFormatException nfe) {
            s_logger.error("Could not parse futures code {}", code);
        }
        return null;
    }

    //-------------------------------------------------------------------------
    /**
     * Resolves the data provider name.
     * 
     * @param dataProvider the data provider, null returns the unknown value
     * @return the resolver data provider, not null
     */
    public static String resolveDataProvider(final String dataProvider) {
        return (dataProvider == null || dataProvider.equalsIgnoreCase(DATA_PROVIDER_UNKNOWN)
                || dataProvider.equalsIgnoreCase(DEFAULT_DATA_PROVIDER) ? DEFAULT_DATA_PROVIDER : dataProvider);
    }

    /**
     * Resolves the data provider to provide an observation time.
     * 
     * @param dataProvider the data provider, null returns the unknown value
     * @return the corresponding observation time for the given data provider
     */
    public static String resolveObservationTime(String dataProvider) {
        if (dataProvider == null || dataProvider.equalsIgnoreCase(DATA_PROVIDER_UNKNOWN)
                || dataProvider.equalsIgnoreCase(DEFAULT_DATA_PROVIDER)) {
            dataProvider = DEFAULT_DATA_PROVIDER;
        }
        return s_observationTimeMap.get(dataProvider);

    }

    private static DateTimeFormatter s_bloombergDateFormatter = new DateTimeFormatterBuilder()
            .parseCaseInsensitive().appendValue(YEAR, 4).appendValue(MONTH_OF_YEAR, 2).appendValue(DAY_OF_MONTH, 2)
            .toFormatter();

    public static String toBloombergDate(LocalDate localDate) {
        localDate = localDate.withYear(Math.min(9999, localDate.getYear()));
        return localDate.toString(s_bloombergDateFormatter);
    }

    /**
     * Returns future month code for a given month
     * 
     * @param month the month of year, not null
     * @return the future month code, null if not available
     */
    public static String futureMonthCode(final Month month) {
        return s_monthCode.get(month);
    }

}