package org.marketcetera.photon; import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.lang.Validate; import org.eclipse.emf.common.notify.Adapter; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.common.notify.impl.AdapterImpl; import org.eclipse.emf.ecore.EAttribute; import org.marketcetera.core.position.MarketDataSupport; import org.marketcetera.photon.marketdata.IMarketData; import org.marketcetera.photon.marketdata.IMarketDataReference; import org.marketcetera.photon.model.marketdata.MDLatestTick; import org.marketcetera.photon.model.marketdata.MDMarketstat; import org.marketcetera.photon.model.marketdata.MDPackage; import org.marketcetera.trade.Future; import org.marketcetera.trade.Instrument; import org.marketcetera.trade.Option; import org.marketcetera.trade.SecurityType; import org.marketcetera.util.misc.ClassVersion; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; /* $License$ */ /** * Implements MarketDataSupport for the position engine in Photon. Market data is provided by the * common marketdata infrastructure in {@link IMarketData}. * * @author <a href="mailto:will@marketcetera.com">Will Horn</a> * @version $Id: PhotonPositionMarketData.java 16854 2014-03-12 01:54:42Z colin $ * @since 1.5.0 */ @ClassVersion("$Id: PhotonPositionMarketData.java 16854 2014-03-12 01:54:42Z colin $") public class PhotonPositionMarketData implements MarketDataSupport { private final IMarketData mMarketData; private final AtomicBoolean mDisposed = new AtomicBoolean(); private final Adapter mLatestTickAdapter = new LatestTickAdapter(); private final Adapter mClosingPriceAdapter = new ClosingPriceAdapter(); /* * mListeners synchronizes access to the following three collections. */ private final SetMultimap<Instrument, InstrumentMarketDataListener> mListeners = HashMultimap.create(); private final Map<Instrument, IMarketDataReference<MDLatestTick>> mLatestTickReferences = Maps .newHashMap(); private final Map<Instrument, IMarketDataReference<MDMarketstat>> mStatReferences = Maps .newHashMap(); /* * These caches allow easy implementation of getLastTradePrice, * getClosingPrice, getOptionMultiplier and getFutureMultiplier. They also allow notification to * be fired only when the values change to avoid unnecessary notifications, * which is especially important with closing price and option multiplier * that rarely change. */ private final ConcurrentMap<Instrument, BigDecimal> mLatestTickCache = new ConcurrentHashMap<Instrument, BigDecimal>(); private final ConcurrentMap<Instrument, BigDecimal> mClosingPriceCache = new ConcurrentHashMap<Instrument, BigDecimal>(); private final ConcurrentMap<Instrument, BigDecimal> mOptionMultiplierCache = new ConcurrentHashMap<Instrument, BigDecimal>(); private final ConcurrentMap<Instrument, BigDecimal> mFutureMultiplierCache = new ConcurrentHashMap<Instrument, BigDecimal>(); /* * Marks null price for the ConcurrentMap caches which don't allow null. This is better than * removing keys since it allows the concurrent put method to be used in {@link * #fireIfChanged(String, BigDecimal, ConcurrentMap, boolean)} */ private static final BigDecimal NULL = new BigDecimal(Integer.MIN_VALUE); /** * Constructor. * * @param marketData * the market data provider * @throws IllegalArgumentException * if marketData is null */ public PhotonPositionMarketData(IMarketData marketData) { Validate.notNull(marketData); mMarketData = marketData; } @Override public BigDecimal getLastTradePrice(Instrument instrument) { Validate.notNull(instrument); // implementation choice to only return the last trade price if it's already known // not worth it to set up a new data flow return getCachedValue(mLatestTickCache, instrument); } @Override public BigDecimal getClosingPrice(Instrument instrument) { Validate.notNull(instrument); // implementation choice to only return the closing price if it's already known // not worth it to set up a new data flow return getCachedValue(mClosingPriceCache, instrument); } @Override public BigDecimal getOptionMultiplier(Option option) { Validate.notNull(option); // implementation choice to only return the multiplier if it's already known // not worth it to set up a new data flow return getCachedValue(mOptionMultiplierCache, option); } @Override public BigDecimal getFutureMultiplier(Future future) { Validate.notNull(future); // implementation choice to only return the multiplier if it's already known // not worth it to set up a new data flow return getCachedValue(mFutureMultiplierCache, future); } private BigDecimal getCachedValue(final ConcurrentMap<Instrument, BigDecimal> cache, final Instrument symbol) { BigDecimal cached = cache.get(symbol); return cached == NULL ? null : cached; } @Override public void addInstrumentMarketDataListener(Instrument inInstrument, InstrumentMarketDataListener inListener) { Validate.noNullElements(new Object[] { inInstrument, inListener }); synchronized(mListeners) { if(mDisposed.get()) return; IMarketDataReference<MDLatestTick> ref = mLatestTickReferences.get(inInstrument); if(ref == null) { ref = mMarketData.getLatestTick(inInstrument); if(ref != null && ref.get() != null) { mLatestTickReferences.put(inInstrument, ref); ref.get().eAdapters().add(mLatestTickAdapter); } } IMarketDataReference<MDMarketstat> statRef = mStatReferences.get(inInstrument); if(statRef == null) { statRef = mMarketData.getMarketstat(inInstrument); mStatReferences.put(inInstrument, statRef); if(statRef != null && statRef.get() != null) { statRef.get().eAdapters().add(mClosingPriceAdapter); } } mListeners.put(inInstrument, inListener); } } @Override public void removeInstrumentMarketDataListener(Instrument instrument, InstrumentMarketDataListener listener) { Validate.noNullElements(new Object[] { instrument, listener }); List<IMarketDataReference<?>> toDispose = Lists.newArrayList(); synchronized (mListeners) { IMarketDataReference<MDLatestTick> ref = mLatestTickReferences.get(instrument); IMarketDataReference<MDMarketstat> statRef = mStatReferences.get(instrument); Set<InstrumentMarketDataListener> listeners = mListeners.get(instrument); listeners.remove(listener); if (listeners.isEmpty()) { if (ref != null) { MDLatestTick tick = ref.get(); if (tick != null) { tick.eAdapters().remove(mLatestTickAdapter); mLatestTickReferences.remove(instrument); mLatestTickCache.remove(instrument); toDispose.add(ref); } } if (statRef != null) { MDMarketstat stat = statRef.get(); if (stat != null) { stat.eAdapters().remove(mClosingPriceAdapter); mStatReferences.remove(instrument); mClosingPriceCache.remove(instrument); toDispose.add(statRef); } } } } // dispose outside of the lock to avoid deadlock for(IMarketDataReference<?> ref : toDispose) { ref.dispose(); } } private void fireSymbolTraded(final MDLatestTick item) { Instrument instrument = item.getInstrument(); BigDecimal newValue = item.getPrice(); if (updateCache(instrument, newValue, mLatestTickCache)) { InstrumentMarketDataEvent event = new InstrumentMarketDataEvent(this, newValue); synchronized (mListeners) { if (mDisposed.get()) return; for (InstrumentMarketDataListener listener : mListeners.get(instrument)) { listener.symbolTraded(event); } } } } private void fireClosingPriceChange(final MDMarketstat item) { Instrument instrument = item.getInstrument(); BigDecimal newValue = item.getPreviousClosePrice(); if (updateCache(instrument, newValue, mClosingPriceCache)) { InstrumentMarketDataEvent event = new InstrumentMarketDataEvent(this, newValue); synchronized (mListeners) { if (mDisposed.get()) return; for (InstrumentMarketDataListener listener : mListeners.get(instrument)) { listener.closePriceChanged(event); } } } } private void fireMultiplierChanged(final MDLatestTick item) { Instrument instrument = item.getInstrument(); BigDecimal newValue = item.getMultiplier(); if(item.getInstrument().getSecurityType()==SecurityType.Option){ if (updateCache(instrument, newValue, mOptionMultiplierCache)) { InstrumentMarketDataEvent event = new InstrumentMarketDataEvent(this, newValue); synchronized (mListeners) { if (mDisposed.get()) return; for (InstrumentMarketDataListener listener : mListeners.get(instrument)) { listener.optionMultiplierChanged(event); } } } } if(item.getInstrument().getSecurityType()==SecurityType.Future){ if (updateCache(instrument, newValue, mFutureMultiplierCache)) { InstrumentMarketDataEvent event = new InstrumentMarketDataEvent(this, newValue); synchronized (mListeners) { if (mDisposed.get()) return; for (InstrumentMarketDataListener listener : mListeners.get(instrument)) { listener.futureMultiplierChanged(event); } } } } } /** * Updates an internal cache and returns whether the value changed. */ private boolean updateCache(final Instrument instrument, BigDecimal newValue, final ConcurrentMap<Instrument,BigDecimal> cache) { BigDecimal oldValue = cache.put(instrument, newValue == null ? NULL : newValue); if(oldValue == NULL) { oldValue = null; } // only notify if the value changed if(oldValue == null && newValue == null) { return false; } else if (oldValue != null && newValue != null && oldValue.compareTo(newValue) == 0) { return false; } return true; } @Override public void dispose() { if (mDisposed.compareAndSet(false, true)) { Set<Map.Entry<Instrument, InstrumentMarketDataListener>> entries; synchronized (mListeners) { // make a copy since we will be modifying mListeners entries = Sets.newHashSet(mListeners .entries()); } for (Map.Entry<Instrument, InstrumentMarketDataListener> entry : entries) { removeInstrumentMarketDataListener(entry.getKey(), entry.getValue()); } } } private class LatestTickAdapter extends AdapterImpl { @Override public void notifyChanged(Notification msg) { if (!msg.isTouch() && msg.getEventType() == Notification.SET) { MDLatestTick item = (MDLatestTick) msg.getNotifier(); if (msg.getFeature() == MDPackage.Literals.MD_LATEST_TICK__PRICE) { fireSymbolTraded(item); } else if (msg.getFeature() == MDPackage.Literals.MD_LATEST_TICK__MULTIPLIER) { fireMultiplierChanged(item); } } } } private class ClosingPriceAdapter extends AdapterImpl { private final ImmutableSet<EAttribute> mAttributes = ImmutableSet.of( MDPackage.Literals.MD_MARKETSTAT__CLOSE_DATE, MDPackage.Literals.MD_MARKETSTAT__CLOSE_PRICE, MDPackage.Literals.MD_MARKETSTAT__PREVIOUS_CLOSE_DATE, MDPackage.Literals.MD_MARKETSTAT__PREVIOUS_CLOSE_PRICE); @Override public void notifyChanged(Notification msg) { if (!msg.isTouch() && msg.getEventType() == Notification.SET && mAttributes.contains(msg.getFeature())) { MDMarketstat item = (MDMarketstat) msg.getNotifier(); fireClosingPriceChange(item); } } } }