package org.marketcetera.strategy.util; import static org.marketcetera.strategy.Messages.WRONG_DIVIDEND_EQUITY_FOR_OPTION_CHAIN; import static org.marketcetera.strategy.Messages.WRONG_EQUITY_FOR_OPTION_CHAIN; import static org.marketcetera.strategy.Messages.WRONG_UNDERLYING_FOR_OPTION_CHAIN; import java.util.*; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.CopyOnWriteArrayList; import javax.annotation.concurrent.ThreadSafe; import org.apache.commons.lang.SystemUtils; import org.marketcetera.core.Pair; import org.marketcetera.event.*; import org.marketcetera.event.util.MarketstatEventCache; import org.marketcetera.strategy.util.OptionContractPair.OptionContractPairKey; import org.marketcetera.trade.Instrument; import org.marketcetera.util.misc.ClassVersion; import org.nocrala.tools.texttablefmt.BorderStyle; import org.nocrala.tools.texttablefmt.CellStyle; import org.nocrala.tools.texttablefmt.ShownBorders; import org.nocrala.tools.texttablefmt.Table; import org.nocrala.tools.texttablefmt.CellStyle.HorizontalAlign; /* $License$ */ /** * Represents the option chain of a given underlying instrument. * * <p>This object maintains an in-memory representation of the option chain * of an underlying instrument and its market data. To use <code>OptionChain</code>, * create a new <code>OptionChain</code> (may use any instrument as the underlying instrument * of the option chain): * <pre> * Equity theEquity = new Equity("GOOG"); * OptionChain theChain = new OptionChain(theEquity); * </pre> * Set up market data for the underlying instrument as normal. As market data events arrive, * pass the appropriate ones to the <code>OptionChain</code>: * <pre> * public void onAsk(AskEvent ask) * { * theChain.process(ask); * } * </pre> * Note that if the <code>AskEvent</code> is not relevant to the <code>OptionChain</code>, it * will be discarded. To take full advantage of the <code>OptionChain</code> object, add * similar code to <code>onBid</code>, <code>onTrade</code>, <code>onMarketstat</code>, and * <code>onDividend</code>. * * <p>The data stored in the <code>OptionChain</code> object can be retrieved as follows: * <pre> * List<OptionContractPair> optionChain = theChain.getOptionChain(); * for(OptionContractPair contractPair : optionChain) { * OptionContract putSide = contractPair.getPut(); * // do something with the put contract * OptionContract callSide = contractPair.getCall(); * // do something with the call contract * } * </pre> * As new market data events come in, the option chain view is updated as the * events are added to the <code>OptionChain</code> object with {@link OptionChain#process(Event)}. * * <p>Dividends for the underlying instrument of the <code>OptionChain</code> object are available in * a similar fashion: * <pre> * List<DividendEvent> dividends = theChain.getDividends(); * </pre> * The <code>OptionChain</code> also tracks market data for the underlying instrument and each * <code>OptionContract</code>. * <pre> * // the latest ask for the option chain underlying instrument * AskEvent ask = theChain.getLatestUnderlyingAsk(); * for(OptionContractPair contractPair : optionChain) { * // the latest ask for the put side of one of the entries in the option chain * ask = contractPair.getPut().getLatestAsk(); * } * </pre> * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: OptionChain.java 16154 2012-07-14 16:34:05Z colin $ * @since 2.0.0 */ @ThreadSafe @ClassVersion("$Id: OptionChain.java 16154 2012-07-14 16:34:05Z colin $") public final class OptionChain { /** * Create a new OptionChain instance. * * @param inUnderlyingInstrument an <code>Instrument</code> value indicating the <code>Instrument</code> for which * to create the <code>OptionChain</code> * @throws NullPointerException if <code>inUnderlyingInstrument</code> is <code>null</code> */ public OptionChain(Instrument inUnderlyingInstrument) { if(inUnderlyingInstrument == null) { throw new NullPointerException(); } instrument = inUnderlyingInstrument; latestMarketstat = new MarketstatEventCache(instrument); } /** * Gets a live, unmodifiable view of the option chain. * * <p>Updates to the option chain will be visible in this view. The * elements in the option chain will be sorted according to the * {@link OptionContractPair} <em>natural order</em>. * * <p>This view is populated when {@link Event} objects are passed * to {@link OptionChain#process(Event)}. * * @return a <code>Collection<OptionContractPair></code> value */ public Collection<OptionContractPair> getOptionChain() { // intentionally returning a live view of the option chain return Collections.unmodifiableCollection(optionChain.values()); } /** * Gets a live, unmodifiable view of the dividends for the underlying instrument. * * <p>Updates to the dividend data for the underlying instrument will be * visible in this view. The elements in the list are sorted in the order * that the corresponding <code>DividendEvent</code> objects are received. * * <p>This view is populated when {@link DividendEvent} objects are passed * to {@link OptionChain#process(Event)}. * * @return a <code>List<DividendEvent></code> value */ public List<DividendEvent> getDividends() { // intentionally returning a live view of the dividends return Collections.unmodifiableList(dividends); } /** * Gets the underlying instrument for this <code>OptionChain</code>. * * @return an <code>Instrument</code> value */ public Instrument getUnderlyingInstrument() { return instrument; } /** * Gets the latest <code>Ask</code> for the underlying instrument. * * <p>This data is populated when {@link AskEvent} objects are passed * to {@link OptionChain#process(Event)}. * * @return an <code>AskEvent</code> or <code>null</code> */ public AskEvent getLatestUnderlyingAsk() { return latestAsk; } /** * Gets the latest <code>Bid</code> for the underlying instrument. * * <p>This data is populated when {@link BidEvent} objects are passed * to {@link OptionChain#process(Event)}. * * @return a <code>BidEvent</code> or <code>null</code> */ public BidEvent getLatestUnderlyingBid() { return latestBid; } /** * Gets the latest <code>Trade</code> for the underlying instrument. * * <p>This data is populated when {@link TradeEvent} objects are passed * to {@link OptionChain#process(Event)}. * * @return a <code>TradeEvent</code> or <code>null</code> */ public TradeEvent getLatestUnderlyingTrade() { return latestTrade; } /** * Gets the latest <code>Marketstat</code> for the underlying instrument. * * <p>This data is populated when {@link MarketstatEvent} objects are passed * to {@link OptionChain#process(Event)}. * * @return a <code>BidEvent</code> or <code>null</code> */ public MarketstatEvent getLatestUnderlyingMarketstat() { return latestMarketstat.get(); } /** * Attempts to apply the given event to this <code>OptionChain</code>. * * @param inEvent an <code>Event</code> value * @return a <code>boolean</code> value which, if true, indicates that the given event was successfully applied * to the option chain. If false, the event was not applicable. */ public boolean process(Event inEvent) { if(inEvent == null) { throw new NullPointerException(); } if(inEvent instanceof AskEvent) { return processAskEvent((AskEvent)inEvent); } if(inEvent instanceof BidEvent) { return processBidEvent((BidEvent)inEvent); } if(inEvent instanceof DividendEvent) { return processDividendEvent((DividendEvent)inEvent); } if(inEvent instanceof MarketstatEvent) { return processMarketstatEvent((MarketstatEvent)inEvent); } if(inEvent instanceof TradeEvent) { return processTradeEvent((TradeEvent)inEvent); } return false; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { // renders the option chain as a human-readable table StringBuilder builder = new StringBuilder(); builder.append(getUnderlyingInstrument().getSymbol()).append(nl); //$NON-NLS-1$ builder.append(BID).append(getLatestUnderlyingBid() == null ? none : String.format("%s %s %s", //$NON-NLS-1$ //$NON-NLS-2$ getLatestUnderlyingBid().getSize(), getLatestUnderlyingBid().getPrice(), getLatestUnderlyingBid().getExchange())).append(nl); builder.append(ASK).append(getLatestUnderlyingAsk() == null ? none : String.format("%s %s %s", //$NON-NLS-1$ //$NON-NLS-2$ getLatestUnderlyingAsk().getSize(), getLatestUnderlyingAsk().getPrice(), getLatestUnderlyingAsk().getExchange())).append(nl); builder.append(LAST).append(getLatestUnderlyingTrade() == null ? none : String.format("%s %s %s", //$NON-NLS-1$ //$NON-NLS-2$ getLatestUnderlyingTrade().getSize(), getLatestUnderlyingTrade().getPrice(), getLatestUnderlyingTrade().getExchange())).append(nl); MarketstatEvent latestUnderlyingStats = getLatestUnderlyingMarketstat(); builder.append(HIGH).append(latestUnderlyingStats == null || latestUnderlyingStats.getHigh() == null ? none : latestUnderlyingStats.getHigh().toPlainString()).append(nl); builder.append(LOW).append(latestUnderlyingStats == null || latestUnderlyingStats.getLow() == null ? none : latestUnderlyingStats.getLow().toPlainString()).append(nl); if(!dividends.isEmpty()) { // add dividends builder.append(DIVIDEND_HEADER).append(nl); Table table = new Table(dividendHeaders.length, BorderStyle.CLASSIC_COMPATIBLE_WIDE, ShownBorders.ALL, false); for(String header : dividendHeaders) { table.addCell(header, headerStyle); } for(DividendEvent dividend : dividends) { table.addCell(dividend.getType() == null ? none : dividend.getType().toString()); table.addCell(dividend.getAmount() == null ? none : String.format("%s (%s)", //$NON-NLS-1$ dividend.getAmount().toPlainString(), dividend.getCurrency())); table.addCell(dividend.getExecutionDate() == null ? none : dividend.getExecutionDate()); table.addCell(dividend.getDeclareDate() == null ? none : dividend.getDeclareDate()); table.addCell(dividend.getPaymentDate() == null ? none : dividend.getPaymentDate()); table.addCell(dividend.getRecordDate() == null ? none : dividend.getRecordDate()); table.addCell(dividend.getStatus() == null ? none : dividend.getStatus().toString()); table.addCell(dividend.getFrequency() == null ? none : dividend.getFrequency().toString()); } builder.append(table.render()); builder.append(nl); } Collection<OptionContractPair> chain = getOptionChain(); if(chain.isEmpty()) { return builder.toString(); } builder.append(OPTION_CHAIN_HEADER).append(nl); Table table = new Table(13, BorderStyle.CLASSIC_COMPATIBLE_WIDE, ShownBorders.ALL, false); // add column headers for(Pair<String,Integer> header : chainHeaders) { table.addCell(header.getFirstMember(), headerStyle, header.getSecondMember()); } for(OptionContractPair pair : chain) { OptionContract put = pair.getPut(); OptionContract call = pair.getCall(); String symbol; String expiry; String strike; String providerSymbol = null; if(put != null) { symbol = put.getInstrument().getSymbol(); expiry = put.getInstrument().getExpiry(); strike = put.getInstrument().getStrikePrice().toPlainString(); if(put.getProviderSymbol() != null) { providerSymbol = put.getProviderSymbol(); } } else if(call != null) { symbol = call.getInstrument().getSymbol(); expiry = call.getInstrument().getExpiry(); strike = call.getInstrument().getStrikePrice().toPlainString(); if(call.getProviderSymbol() != null) { providerSymbol = call.getProviderSymbol(); } } else { continue; } StringBuilder cell = new StringBuilder(); cell.append(symbol).append(" ").append(expiry).append(" ").append(strike); if(providerSymbol != null) { cell.append(" (").append(providerSymbol).append(")"); } table.addCell(cell.toString()); if(put != null && put.getLatestBid() != null) { BidEvent bid = put.getLatestBid(); table.addCell(bid.getSize().toPlainString()); table.addCell(String.format("%s %s", //$NON-NLS-1$ bid.getPrice().toPlainString(), bid.getExchange())); } else { table.addCell(none); table.addCell(none); } if(put != null && put.getLatestAsk() != null) { AskEvent ask = put.getLatestAsk(); table.addCell(ask.getSize().toPlainString()); table.addCell(String.format("%s %s", //$NON-NLS-1$ ask.getPrice().toPlainString(), ask.getExchange())); } else { table.addCell(none); table.addCell(none); } if(put != null && put.getLatestTrade() != null) { TradeEvent trade = put.getLatestTrade(); table.addCell(trade.getSize().toPlainString()); table.addCell(String.format("%s %s", //$NON-NLS-1$ trade.getPrice().toPlainString(), trade.getExchange())); } else { table.addCell(none); table.addCell(none); } if(call != null && call.getLatestBid() != null) { BidEvent bid = call.getLatestBid(); table.addCell(bid.getSize().toPlainString()); table.addCell(String.format("%s %s", //$NON-NLS-1$ bid.getPrice().toPlainString(), bid.getExchange())); } else { table.addCell(none); table.addCell(none); } if(call != null && call.getLatestAsk() != null) { AskEvent ask = call.getLatestAsk(); table.addCell(ask.getSize().toPlainString()); table.addCell(String.format("%s %s", //$NON-NLS-1$ ask.getPrice().toPlainString(), ask.getExchange())); } else { table.addCell(none); table.addCell(none); } if(call != null && call.getLatestTrade() != null) { TradeEvent trade = call.getLatestTrade(); table.addCell(trade.getSize().toPlainString()); table.addCell(String.format("%s %s", //$NON-NLS-1$ trade.getPrice().toPlainString(), trade.getExchange())); } else { table.addCell(none); table.addCell(none); } } builder.append(table.render()); return builder.toString(); } /** * Processes the given <code>AskEvent</code>. * * @param inAsk an <code>AskEvent</code> value * @return a <code>boolean</code> value indicating whether the <code>AskEvent</code> was successfully * applied or not */ private boolean processAskEvent(AskEvent inAsk) { if(!validate(inAsk)) { return false; } if(inAsk instanceof EquityEvent) { latestAsk = inAsk; return true; } if(inAsk instanceof OptionEvent) { return processEventForOptionChain((OptionEvent)inAsk); } return false; } /** * Processes the given <code>BidEvent</code>. * * @param inBid a <code>BidEvent</code> value * @return a <code>boolean</code> value indicating whether the <code>BidEvent</code> was successfully * applied or not */ private boolean processBidEvent(BidEvent inBid) { if(!validate(inBid)) { return false; } if(inBid instanceof EquityEvent) { latestBid = inBid; return true; } if(inBid instanceof OptionEvent) { return processEventForOptionChain((OptionEvent)inBid); } return false; } /** * Processes the given <code>DividendEvent</code>. * * @param inDividend a <code>DividendEvent</code> value * @return a <code>boolean</code> value indicating whether the <code>DividendEvent</code> was successfully * applied or not */ private boolean processDividendEvent(DividendEvent inDividend) { if(!validate(inDividend)) { return false; } dividends.add(inDividend); return true; } /** * Processes the given <code>MarketstatEvent</code>. * * @param inMarketstat a <code>MarketstatEvent</code> value * @return a <code>boolean</code> value indicating whether the <code>MarketstatEvent</code> was successfully * applied or not */ private boolean processMarketstatEvent(MarketstatEvent inMarketstat) { if(!validate(inMarketstat)) { return false; } if(inMarketstat instanceof EquityEvent) { latestMarketstat.cache(inMarketstat); return true; } if(inMarketstat instanceof OptionEvent) { return processEventForOptionChain((OptionEvent)inMarketstat); } return false; } /** * Processes the given <code>TradeEvent</code>. * * @param inTrade a <code>TradeEvent</code> value * @return a <code>boolean</code> value indicating whether the <code>TradeEvent</code> was successfully * applied or not */ private boolean processTradeEvent(TradeEvent inTrade) { if(!validate(inTrade)) { return false; } if(inTrade instanceof EquityEvent) { latestTrade = inTrade; return true; } if(inTrade instanceof OptionEvent) { return processEventForOptionChain((OptionEvent)inTrade); } return false; } /** * Processes the given <code>OptionEvent</code> to update the option chain. * * @param inOptionEvent an <code>OptionEvent</code> value * @return a <code>boolean</code> value indicating whether the <code>OptionEvent</code> was successfully * applied or not */ private synchronized boolean processEventForOptionChain(OptionEvent inOptionEvent) { // this method is synchronized in order to make put-if-absent atomic // create the object that will identify the correct contract pair in the option chain - the pair // may or may not yet exist in the chain OptionContractPairKey key = OptionContractPair.getOptionContractPairKey(inOptionEvent.getInstrument()); // retrieve the pair if it exists OptionContractPair contractPair = optionChain.get(key); if(contractPair == null) { // the pair does not yet exist, create the put/call contract pair for this event contractPair = new OptionContractPair(inOptionEvent); // add the new contract pair to the chain optionChain.put(key, contractPair); } // the contract pair exists in the pair (is non-null, here, too) - process the option event return contractPair.process(inOptionEvent); } /** * Validates that the given event is applicable to this <code>OptionChain</code>. * * @param inEvent an <code>Event</code> value * @return a <code>boolean</code> value indicating if the event is valid */ private boolean validate(Event inEvent) { if(inEvent instanceof OptionEvent) { OptionEvent optionEvent = (OptionEvent)inEvent; if(!optionEvent.getUnderlyingInstrument().equals(getUnderlyingInstrument())) { WRONG_UNDERLYING_FOR_OPTION_CHAIN.warn(OptionChain.class, optionEvent.getUnderlyingInstrument(), getUnderlyingInstrument()); return false; } } if(inEvent instanceof DividendEvent) { DividendEvent dividendEvent = (DividendEvent)inEvent; if(!dividendEvent.getInstrument().equals(getUnderlyingInstrument())) { WRONG_DIVIDEND_EQUITY_FOR_OPTION_CHAIN.warn(OptionChain.class, dividendEvent.getInstrument(), getUnderlyingInstrument()); return false; } } if(inEvent instanceof EquityEvent) { EquityEvent equityEvent = (EquityEvent)inEvent; if(!equityEvent.getInstrument().equals(getUnderlyingInstrument())) { WRONG_EQUITY_FOR_OPTION_CHAIN.warn(OptionChain.class, equityEvent.getInstrument(), getUnderlyingInstrument()); return false; } } return true; } /** * the option chain - the collection of record for the option chain - made concurrent in order for it to be returned * outside the scope of this object and still predictably reflect updates, potentially in different threads */ private final Map<OptionContractPairKey,OptionContractPair> optionChain = new ConcurrentSkipListMap<OptionContractPairKey,OptionContractPair>(); /** * the live view of the dividend data - dividends should have relatively few writes but many more traversals. copy-on-write makes * for expensive writes but allows concurrent reads */ private final List<DividendEvent> dividends = new CopyOnWriteArrayList<DividendEvent>(); /** * the instrument for which to hold an option chain */ private final Instrument instrument; /** * the latest ask for the option chain underlying instrument, may be <code>null</code> */ private volatile AskEvent latestAsk = null; /** * the latest bid for the option chain underlying instrument, may be <code>null</code> */ private volatile BidEvent latestBid = null; /** * the latest marketstat for the option chain underlying instrument, may be <code>null</code> */ private final MarketstatEventCache latestMarketstat; /** * the latest trade for the option chain underlying instrument, may be <code>null</code> */ private volatile TradeEvent latestTrade = null; // the following are constants used to display the option chain private static final String nl = SystemUtils.LINE_SEPARATOR; private static final String none = "---"; //$NON-NLS-1$ private static final CellStyle headerStyle = new CellStyle(HorizontalAlign.center); private static final String[] dividendHeaders = new String[] { "Type","Amount","Execution Date","Declare Date","Payment Date","Record Date","Status","Frequency" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ private static final List<Pair<String,Integer>> chainHeaders = new ArrayList<Pair<String,Integer>>(); private static final String BID = "Bid: "; //$NON-NLS-1$ private static final String ASK = "Ask: "; //$NON-NLS-1$ private static final String LAST = "Last: "; //$NON-NLS-1$ private static final String HIGH = "High: "; //$NON-NLS-1$ private static final String LOW = "Low: "; //$NON-NLS-1$ private static final String DIVIDEND_HEADER = "Dividends"; //$NON-NLS-1$ private static final String OPTION_CHAIN_HEADER = "Option Chain"; //$NON-NLS-1$ static { chainHeaders.add(new Pair<String,Integer>("", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Put", //$NON-NLS-1$ 6)); chainHeaders.add(new Pair<String,Integer>("Call", //$NON-NLS-1$ 6)); chainHeaders.add(new Pair<String,Integer>("", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Bid", //$NON-NLS-1$ 2)); chainHeaders.add(new Pair<String,Integer>("Ask", //$NON-NLS-1$ 2)); chainHeaders.add(new Pair<String,Integer>("Latest", //$NON-NLS-1$ 2)); chainHeaders.add(new Pair<String,Integer>("Bid", //$NON-NLS-1$ 2)); chainHeaders.add(new Pair<String,Integer>("Ask", //$NON-NLS-1$ 2)); chainHeaders.add(new Pair<String,Integer>("Latest", //$NON-NLS-1$ 2)); chainHeaders.add(new Pair<String,Integer>("Symbol/Expiry/Strike", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Size", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Price X", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Size", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Price X", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Size", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Price X", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Size", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Price X", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Size", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Price X", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Size", //$NON-NLS-1$ 1)); chainHeaders.add(new Pair<String,Integer>("Price X", //$NON-NLS-1$ 1)); } }