package org.marketcetera.marketdata; import static org.marketcetera.marketdata.Messages.DIVIDEND_REQUEST_MISSING_INSTRUMENT; import static org.marketcetera.marketdata.Messages.SIMULATED_EXCHANGE_CODE_MISMATCH; import static org.marketcetera.marketdata.Messages.SIMULATED_EXCHANGE_OUT_OF_EVENTS; import static org.marketcetera.marketdata.Messages.SIMULATED_EXCHANGE_SKIPPED_EVENT; import static org.marketcetera.marketdata.Messages.SIMULATED_EXCHANGE_TICK_ERROR; import static org.marketcetera.marketdata.Messages.STARTING_RANDOM_EXCHANGE; import static org.marketcetera.marketdata.Messages.STARTING_SCRIPTED_EXCHANGE; import static org.marketcetera.marketdata.Messages.STOPPING_SIMULATED_EXCHANGE; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.ThreadSafe; import org.marketcetera.core.Pair; import org.marketcetera.core.publisher.ISubscriber; import org.marketcetera.core.publisher.PublisherEngine; import org.marketcetera.event.AskEvent; import org.marketcetera.event.BidEvent; import org.marketcetera.event.DividendEvent; import org.marketcetera.event.DividendFrequency; import org.marketcetera.event.DividendStatus; import org.marketcetera.event.DividendType; import org.marketcetera.event.Event; import org.marketcetera.event.EventType; import org.marketcetera.event.HasEventType; import org.marketcetera.event.HasInstrument; import org.marketcetera.event.HasUnderlyingInstrument; import org.marketcetera.event.MarketDataEvent; import org.marketcetera.event.MarketstatEvent; import org.marketcetera.event.OptionEvent; import org.marketcetera.event.QuoteEvent; import org.marketcetera.event.TradeEvent; import org.marketcetera.event.impl.DividendEventBuilder; import org.marketcetera.event.impl.MarketstatEventBuilder; import org.marketcetera.event.impl.QuoteEventBuilder; import org.marketcetera.event.impl.TradeEventBuilder; import org.marketcetera.event.util.PriceAndSizeComparator; import org.marketcetera.options.ExpirationType; import org.marketcetera.trade.DeliveryType; import org.marketcetera.trade.Equity; import org.marketcetera.trade.Future; import org.marketcetera.trade.Instrument; import org.marketcetera.trade.Option; import org.marketcetera.trade.StandardType; import org.marketcetera.util.log.SLF4JLoggerProxy; import org.marketcetera.util.misc.ClassVersion; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; /* $License$ */ /** * Simulates an exchange with managed order books for multiple instruments. * * <p>This exchange manages a separate order book for each instrument for which requests are made. * If a request is made for a instrument which is not yet managed, an order book seeded with a random * starting price will be added. The order book for a instrument will continue to be maintained until * the exchange is stopped via {@link SimulatedExchange#stop()}. * * <p>To begin simulating an exchange, instantiate the exchange with a specified book depth. The * exchange will simulate market activity to the specified book depth following the rules set * down in {@link OrderBook} for maximum depth. It is generally a good idea to limit memory consumption * by setting the maximum depth to 10 or so. * * <p>The exchange has two modes in which it can operate: <em>scripted</em> and <em>random</em>. * For scripted mode, the exchange executes a specified script of {@link QuoteEvent} objects. The * events are executed synchronously when the exchange is started. When all events have been executed, * the exchange stays running, but will not execute any further changes. * Scripted mode is invoked by calling {@link SimulatedExchange#start(List)} with a non-empty, non-null * list of {@link QuoteEvent} objects. * * <p>Random mode is invoked by calling {@link SimulatedExchange#start()} or by calling * {@link SimulatedExchange#start(List)} with a null or empty list. In random mode, the exchange will * continue to simulate behavior in a modified Monte Carlo method. The exchange will continue simulating * market data until stopped. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $ * @since 1.5.0 */ @ThreadSafe @ClassVersion("$Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $") public class SimulatedExchange implements Exchange<SimulatedExchange.Token> { /** * Create a new <code>SimulatedExchange</code> instance. * * @param inName a <code>String</code> value containing the name to associate with the exchange * @param inCode a <code>String</code> value containing the exchange code of this exchange * @param inMaxBookDepth an <code>int</code> value containing the maximum depth to maintain for the order books. This value * must conform to the requirements established for {@link OrderBook#OrderBook(org.marketcetera.trade.Instrument,int)}. * @throws IllegalArgumentException if the given <code>inMaxBookDepth</code> does not correspond to a valid {@link OrderBook} maximum depth */ public SimulatedExchange(String inName, String inCode, int inMaxBookDepth) { if(inName == null || inCode == null) { throw new NullPointerException(); } OrderBook.validateMaximumBookDepth(inMaxBookDepth); name = inName; code = inCode; maxDepth = inMaxBookDepth; Multimap<Instrument,FilteringSubscriber> unsynchronizedOptionChainSubscribers = HashMultimap.create(); optionChainSubscribers = Multimaps.synchronizedMultimap(unsynchronizedOptionChainSubscribers); setStatus(Status.STOPPED); } /** * Create a new <code>SimulatedExchange</code> instance with a reasonable maximum book depth. * * <p>The exchange will have a maximum book depth of {@link OrderBook#DEFAULT_DEPTH}. * * @param inName a <code>String</code> value containing the name to associate with the exchange * @param inCode a <code>String</code> value containing the exchange code of this exchange */ public SimulatedExchange(String inName, String inCode) { this(inName, inCode, OrderBook.DEFAULT_DEPTH); } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getName() */ @Override public String getName() { return name; } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getCode() */ @Override public String getCode() { return code; } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getDepthOfBook(org.marketcetera.marketdata.ExchangeRequest) */ @Override public List<QuoteEvent> getDepthOfBook(ExchangeRequest inExchangeRequest) { long startingTime = System.currentTimeMillis(); long requestId = requestCounter.incrementAndGet(); try { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} received synchronous depth-of-book request: {}, assigned id: {}", //$NON-NLS-1$ this, inExchangeRequest, requestId); validateSynchronousRequest(inExchangeRequest); updateInfo(inExchangeRequest); List<QuoteEvent> result = new ArrayList<QuoteEvent>(); for(PrivateInstrumentInfo book : getAllAffectedBooks(inExchangeRequest)) { result.addAll(book.getBook().getDepthOfBook().decompose()); } return result; } finally { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} completed request {} in {}ms", //$NON-NLS-1$ this, requestId, String.valueOf(System.currentTimeMillis() - startingTime)); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getTopOfBook(org.marketcetera.marketdata.ExchangeRequest) */ @Override public List<QuoteEvent> getTopOfBook(ExchangeRequest inExchangeRequest) { long startingTime = System.currentTimeMillis(); long requestId = requestCounter.incrementAndGet(); try { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} received synchronous top-of-book request: {}, assigned id: {}", //$NON-NLS-1$ this, inExchangeRequest, requestId); validateSynchronousRequest(inExchangeRequest); updateInfo(inExchangeRequest); List<QuoteEvent> result = new ArrayList<QuoteEvent>(); for(PrivateInstrumentInfo book : getAllAffectedBooks(inExchangeRequest)) { result.addAll(book.getBook().getTopOfBook().decompose()); } return result; } finally { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} completing request {} in {}ms", //$NON-NLS-1$ this, requestId, String.valueOf(System.currentTimeMillis() - startingTime)); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getLatestTick(org.marketcetera.marketdata.ExchangeRequest) */ @Override public List<TradeEvent> getLatestTick(ExchangeRequest inExchangeRequest) { long startingTime = System.currentTimeMillis(); long requestId = requestCounter.incrementAndGet(); try { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} received synchronous latest tick request: {}, assigned id: {}", //$NON-NLS-1$ this, inExchangeRequest, requestId); validateSynchronousRequest(inExchangeRequest); updateInfo(inExchangeRequest); List<TradeEvent> result = new ArrayList<TradeEvent>(); for(PrivateInstrumentInfo book : getAllAffectedBooks(inExchangeRequest)) { result.add(book.getLatestTrade()); } return result; } finally { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} completing request {} in {}ms", //$NON-NLS-1$ this, requestId, String.valueOf(System.currentTimeMillis() - startingTime)); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getStatistics(org.marketcetera.marketdata.ExchangeRequest) */ @Override public List<MarketstatEvent> getStatistics(ExchangeRequest inExchangeRequest) { long startingTime = System.currentTimeMillis(); long requestId = requestCounter.incrementAndGet(); try { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} received synchronous statistics request: {}, assigned id: {}", //$NON-NLS-1$ this, inExchangeRequest, requestId); validateSynchronousRequest(inExchangeRequest); updateInfo(inExchangeRequest); // to properly implement this behavior, we would need an arbitrary amount of // historical data. there is currently no facility to persist quotes and trades // in this simulated exchange because the cost (memory and performance) does not // justify the benefit List<MarketstatEvent> results = new ArrayList<MarketstatEvent>(); // for now, we'll just return some blatantly random data for(PrivateInstrumentInfo book : getAllAffectedBooks(inExchangeRequest)) { // create a value clustered around the current book value just to make the data a little // more useful // synchronization and accuracy aren't relevant here because the data are random // anyway BigDecimal currentValue = book.getValue(); // determine open and close prices (set to current value +/- 0.00-9.99 inclusive) BigDecimal openPrice = currentValue.add(randomDecimalDifference(10)); if(openPrice.compareTo(BigDecimal.ZERO) == -1) { openPrice = PENNY; } BigDecimal closePrice = currentValue.add(randomDecimalDifference(10)); if(closePrice.compareTo(BigDecimal.ZERO) == -1) { closePrice = PENNY; } BigDecimal previousClosePrice = currentValue.add(randomDecimalDifference(10)); if(previousClosePrice.compareTo(BigDecimal.ZERO) == -1) { previousClosePrice = PENNY; } // calculate high price (the max of current, open, and close + 0.00-4.99 inclusive) BigDecimal highPrice = currentValue.max(openPrice).max(closePrice).add(randomDecimalDifference(5).abs()); // calculate low price (the min of current, open, and close - 0.00-4.99 inclusive) BigDecimal lowPrice = currentValue.min(openPrice).min(closePrice).subtract(randomDecimalDifference(5).abs()); // ready to return the data Instrument requestInstrument = book.getInstrument(); MarketstatEventBuilder builder = MarketstatEventBuilder.marketstat(requestInstrument); builder.withEventType(EventType.UPDATE_PART) .withOpenPrice(openPrice) .withHighPrice(highPrice) .withLowPrice(lowPrice) .withClosePrice(closePrice) .withPreviousClosePrice(previousClosePrice) .withVolume(randomInteger(100000)) .withValue(randomInteger(100000)) .withCloseDate(DateUtils.dateToString(new Date(startingTime-(HOURms*8)))) .withPreviousCloseDate(DateUtils.dateToString(new Date(startingTime-(DAYms)))) .withTradeHighTime(DateUtils.dateToString(new Date(startingTime-(HOURms*4)))) .withTradeLowTime(DateUtils.dateToString(new Date(startingTime-(HOURms*4)))) .withOpenExchange(getCode()) .withHighExchange(getCode()) .withLowExchange(getCode()) .withCloseExchange(getCode()); if(requestInstrument instanceof Option) { SharedInstrumentInfo sharedInfo = getSharedInstrumentInfo(requestInstrument); // this information must already be present (updateInfo creates it) assert(sharedInfo != null); assert(sharedInfo.getUnderlyingInstrument() != null); builder.withExpirationType(getExpirationType((Option)requestInstrument)) .withUnderlyingInstrument(sharedInfo.getUnderlyingInstrument()) .withInterestChange(randomInteger(1000)) .withVolumeChange(randomInteger(1000)); } if(requestInstrument instanceof Future) { builder.withContractSize(100) .withDeliveryType(DeliveryType.PHYSICAL) .withStandardType(StandardType.STANDARD); } results.add(builder.create()); } return results; } finally { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} completing request {} in {}ms", //$NON-NLS-1$ this, requestId, String.valueOf(System.currentTimeMillis() - startingTime)); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getDividends(org.marketcetera.marketdata.ExchangeRequest) */ @Override public List<DividendEvent> getDividends(ExchangeRequest inExchangeRequest) { long startingTime = System.currentTimeMillis(); long requestId = requestCounter.incrementAndGet(); try { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} received synchronous dividends request: {}, assigned id: {}", //$NON-NLS-1$ this, inExchangeRequest, requestId); validateSynchronousRequest(inExchangeRequest); if(inExchangeRequest.getInstrument() == null) { throw new IllegalArgumentException(DIVIDEND_REQUEST_MISSING_INSTRUMENT.getText(inExchangeRequest.toString())); } if(inExchangeRequest.getInstrument() instanceof Option) { throw new IllegalArgumentException(DIVIDEND_REQUEST_MISSING_INSTRUMENT.getText(inExchangeRequest.toString())); } if(inExchangeRequest.getUnderlyingInstrument() != null) { throw new IllegalArgumentException(DIVIDEND_REQUEST_MISSING_INSTRUMENT.getText(inExchangeRequest.toString())); } updateInfo(inExchangeRequest); List<DividendEvent> results = new ArrayList<DividendEvent>(); // note that, in the current implementation, there should be only one affected book // however, the cost of not assuming this is minimal, so pretend there can be more than one // to match the implementation of other synchronous requests for(PrivateInstrumentInfo book : getAllAffectedBooks(inExchangeRequest)) { SharedInstrumentInfo sharedInfo = getSharedInstrumentInfo(book.getInstrument()); assert(sharedInfo != null); results.addAll(sharedInfo.getDividends()); } return results; } finally { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} completing request {} in {}ms", //$NON-NLS-1$ this, requestId, String.valueOf(System.currentTimeMillis() - startingTime)); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getDividends(org.marketcetera.marketdata.ExchangeRequest, org.marketcetera.core.publisher.ISubscriber) */ @Override public Token getDividends(ExchangeRequest inExchangeRequest, ISubscriber inSubscriber) { return doAsynchronousRequest(inExchangeRequest, inSubscriber, Type.DIVIDENDS); } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getStatistics(org.marketcetera.marketdata.ExchangeRequest, org.marketcetera.core.publisher.ISubscriber) */ @Override public Token getStatistics(ExchangeRequest inExchangeRequest, ISubscriber inSubscriber) { return doAsynchronousRequest(inExchangeRequest, inSubscriber, Type.STATISTICS); } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getDepthOfBook(org.marketcetera.marketdata.ExchangeRequest, org.marketcetera.core.publisher.ISubscriber) */ @Override public Token getDepthOfBook(ExchangeRequest inExchangeRequest, ISubscriber inSubscriber) { return doAsynchronousRequest(inExchangeRequest, inSubscriber, Type.DEPTH_OF_BOOK); } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getLatestTick(org.marketcetera.marketdata.ExchangeRequest, org.marketcetera.core.publisher.ISubscriber) */ @Override public Token getLatestTick(ExchangeRequest inExchangeRequest, ISubscriber inSubscriber) { return doAsynchronousRequest(inExchangeRequest, inSubscriber, Type.LATEST_TICK); } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#getTopOfBook(org.marketcetera.marketdata.ExchangeRequest, org.marketcetera.core.publisher.ISubscriber) */ @Override public Token getTopOfBook(ExchangeRequest inExchangeRequest, ISubscriber inSubscriber) { return doAsynchronousRequest(inExchangeRequest, inSubscriber, Type.TOP_OF_BOOK); } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#cancel(java.lang.Object) */ @Override public void cancel(Token inToken) { publisher.unsubscribe(inToken.getSubscriber()); } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#start() */ @Override public void start() { start(null); } /** * Starts the exchange. * * <p>If the given list of events is non-null and non-empty, the exchange * will be started in {@link SimulatedExchange.Status#SCRIPTED} mode * instead of {@link SimulatedExchange.Status#RANDOM} mode. * * <p>In scripted mode, the exchange will take the given events and * process them in order, one per exchange tick. When the list of events * is empty, the exchange is stopped. * * @param inEvents a <code>List<QuoteEvent></code> value * @throws IllegalStateException if the exchange is already running */ public synchronized void start(List<QuoteEvent> inEvents) { if(getStatus().isRunning()) { throw new IllegalStateException(); } // clear the scripted events collection and then add the passed // events if there are any. the contents of the events list passed // in dictates the mode of the exchange if(inEvents != null && !inEvents.isEmpty()) { STARTING_SCRIPTED_EXCHANGE.info(SimulatedExchange.class, getName()); setStatus(Status.SCRIPTED); doScriptedTicks(inEvents); setStatus(Status.COMPLETE); } else { STARTING_RANDOM_EXCHANGE.info(SimulatedExchange.class, getName()); setStatus(Status.RANDOM); // set up a job to run a tick every second until stopped ticker = executor.scheduleAtFixedRate(new Runnable() { public void run() { try { executeTick(); } catch (Exception e) { SIMULATED_EXCHANGE_TICK_ERROR.warn(SimulatedExchange.class, e, getName()); } } }, 0, 1, TimeUnit.SECONDS); // prepare to execute ticks readyForTick.set(true); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.Exchange#stop() */ @Override public synchronized void stop() { try { if(!getStatus().isRunning()) { throw new IllegalStateException(); } STOPPING_SIMULATED_EXCHANGE.info(SimulatedExchange.class, getName()); // turn off the update engine if(ticker != null) { ticker.cancel(true); executor.purge(); } books.clear(); } finally { setStatus(Status.STOPPED); } } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("%s(%s)", //$NON-NLS-1$ getName(), getCode()); } /** * Get the status value. * * @return a <code>Status</code> value */ public Status getStatus() { return status; } /** * Get the max Depth value. * * @return an <code>int</code> value */ public int getMaxDepth() { return maxDepth; } /** * Executes an asynchronous request with the given parameters. * * @param inExchangeRequest an <code>ExchangeRequest</code> value * @param inSubscriber an <code>ISubscriber</code> value * @param inRequestType a <code>Type</code> value * @return a <code>Token</code> value */ private Token doAsynchronousRequest(ExchangeRequest inExchangeRequest, ISubscriber inSubscriber, Type inRequestType) { long startingTime = System.currentTimeMillis(); long requestId = requestCounter.incrementAndGet(); try { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} received asynchronous {} request: {}, assigned id: {}", //$NON-NLS-1$ this, inRequestType, inExchangeRequest, requestId); // validate the request and subscriber validateAsynchronousRequest(inExchangeRequest, inSubscriber); // the request and subscriber are both valid // do any prep work for the instruments in the request updateInfo(inExchangeRequest); // shared and private instrument books are prepared and ready // this is the list that will hold all the books that this request touches List<PrivateInstrumentInfo> affectedBooks = new ArrayList<PrivateInstrumentInfo>(); affectedBooks = getAllAffectedBooks(inExchangeRequest); // list of affected books is complete, now collect the instruments from each book List<Instrument> allAffectedInstruments = new ArrayList<Instrument>(); // the list of instruments affected by this request is complete for(PrivateInstrumentInfo book : affectedBooks) { allAffectedInstruments.add(book.getInstrument()); } Token token = FilteringSubscriber.subscribe(inSubscriber, inRequestType, allAffectedInstruments, this, inExchangeRequest); return token; } finally { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} completing request {} in {}ms", //$NON-NLS-1$ this, requestId, String.valueOf(System.currentTimeMillis() - startingTime)); } } /** * Returns all books involved with the given request. * * <p>Callers are guaranteed that the list of books returned is the complete list * at this time of books affected by the given request. * * <p>All data (common and private) must have already been set up for this method * to complete successfully. The caller must make sure that {@link #updateInfo(HasInstrument)} * has been invoked before calling this method. * * @param inExchangeRequest an <code>ExchangeRequest</code> value * @return a <code>List<PrivateInstrumentInfo></code> value */ private List<PrivateInstrumentInfo> getAllAffectedBooks(ExchangeRequest inExchangeRequest) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} searching for books relevant to {}", //$NON-NLS-1$ this, inExchangeRequest); List<PrivateInstrumentInfo> affectedBooks = new ArrayList<PrivateInstrumentInfo>(); // if the request specifies an instrument, that instrument is the only affected book if(inExchangeRequest.getInstrument() != null) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "The request has a primary instrument {} - this is the only affected book", //$NON-NLS-1$ inExchangeRequest.getInstrument()); affectedBooks.add(getPrivateInstrumentInfo(inExchangeRequest.getInstrument())); } else { // instrument is null, this must be a request for the underlying instrument Instrument underlyingInstrument = inExchangeRequest.getUnderlyingInstrument(); // this request does not specify an instrument, it *must* specify an underlying instrument // or our understanding of ExchangeRequest is faulty assert(underlyingInstrument != null); SLF4JLoggerProxy.debug(SimulatedExchange.class, "The request has no primary instrument, but does have an underlying instrument {}", //$NON-NLS-1$ underlyingInstrument); SharedInstrumentInfo underlyingInfo = getSharedInstrumentInfo(underlyingInstrument); // underlyingInfo must already exist or somebody forgot to call updateInfo assert(underlyingInfo != null); SLF4JLoggerProxy.debug(SimulatedExchange.class, "The underlying instrument has shared info: {}", //$NON-NLS-1$ underlyingInfo); // get the instruments that make up the option chain for(Instrument optionChainInstrument : underlyingInfo.getOptionChain()) { PrivateInstrumentInfo book = getPrivateInstrumentInfo(optionChainInstrument); // the book for the instrument is supposed to already exist, also created during updateInfo assert(book != null); SLF4JLoggerProxy.debug(SimulatedExchange.class, "Adding option chain book: {}", //$NON-NLS-1$ book); affectedBooks.add(book); } } SLF4JLoggerProxy.debug(SimulatedExchange.class, "Returning the following affected books: {}", //$NON-NLS-1$ affectedBooks); return affectedBooks; } /** * Validates that the given <code>ExchangeRequest</code> and <code>ISubscriber</code> are * appropriate to be used for an asynchronous market data request. * * @param inRequest an <code>ExchangeRequest</code> value * @param inSubscriber an <code>ISubscriber</code> value */ private void validateAsynchronousRequest(ExchangeRequest inRequest, ISubscriber inSubscriber) { // no status check for the exchange because subscription requests may be submitted any time if(inSubscriber == null) { throw new NullPointerException(); } doCommonValidation(inRequest); } /** * Validates that the given <code>ExchangeRequest</code> is * appropriate to be used for a synchronous market data request. * * @param inRequest an <code>ExchangeRequest</code> value * @throws IllegalStateException if the exchange is not running */ private void validateSynchronousRequest(ExchangeRequest inRequest) { doCommonValidation(inRequest); // for a synchronous request, the exchange must be running if(!getStatus().isRunning()) { throw new IllegalStateException(); } } /** * Validates that the given <code>ExchangeRequest</code> is * appropriate to be used for any market data request. * * @param inRequest an <code>ExchangeRequest</code> value */ private void doCommonValidation(ExchangeRequest inRequest) { if(inRequest == null) { throw new NullPointerException(); } } /** * Sets the status value. * * @param a <code>Status</code> value */ private void setStatus(Status inStatus) { status = inStatus; } /** * Gets the <code>PrivateInstrumentInfo</code> associated with the given * <code>Instrument</code>. * * <p>The <code>PrivateInstrumentInfo</code> must already exist. It is the * caller's responsibility to make sure this is the case by making sure that * {@link #updateInfo(HasInstrument)} is called before invoking this method. * * @param inInstrument an <code>Instrument</code> value * @return a <code>PrivateInstrumentInfo</code> value */ private PrivateInstrumentInfo getPrivateInstrumentInfo(Instrument inInstrument) { PrivateInstrumentInfo info = books.get(inInstrument); assert(info != null); return info; } /** * Updates shared and private exchange information using the given <code>HasInstrument</code> * value. * * <p>This method will set up the exchange to handle the given instrument. This method * may be called more than once with the same <code>HasInstrument</code> with no ill effect. * * @param inInstrumentProvider a <code>HasInstrument</code> value */ private void updateInfo(HasInstrument inInstrumentProvider) { updateSharedInfo(inInstrumentProvider); updatePrivateInfo(inInstrumentProvider); } /** * Updates the private exchange information using the given <code>HasInstrument</code> value. * * <p>This method will set up the exchange to handle the given instrument. This method * may be called more than once with the same <code>HasInstrument</code> with no ill effect. * * @param inInstrumentProvider a <code>HasInstrument</code> value */ private synchronized void updatePrivateInfo(HasInstrument inInstrumentProvider) { // this method is synchronized because of the put-if-absent performed on books Instrument primaryInstrument = inInstrumentProvider.getInstrument(); // the instrument might be null if we're dealing with underlying-only if(primaryInstrument != null) { PrivateInstrumentInfo book = books.get(inInstrumentProvider.getInstrument()); if(book == null) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} creating book for {}", //$NON-NLS-1$ this, inInstrumentProvider.getInstrument()); book = new PrivateInstrumentInfo(inInstrumentProvider.getInstrument()); books.put(inInstrumentProvider.getInstrument(), book); if(getStatus().isRunning() && getStatus() == Status.RANDOM) { // set some initial data in the book doRandomBookTick(book); } } } // create a book for the underlying instrument, if applicable if(inInstrumentProvider instanceof HasUnderlyingInstrument) { Instrument underlyingInstrument = ((HasUnderlyingInstrument)inInstrumentProvider).getUnderlyingInstrument(); if(underlyingInstrument != null) { // there is an underlying instrument present - make sure it has a book, too PrivateInstrumentInfo underlyingBook = books.get(underlyingInstrument); if(underlyingBook == null) { underlyingBook = new PrivateInstrumentInfo(underlyingInstrument); books.put(underlyingInstrument, underlyingBook); if(getStatus().isRunning() && getStatus() == Status.RANDOM) { // set some initial data in the book doRandomBookTick(underlyingBook); } } // there may be entries in the option chain for the underlying that do // not yet have books in this exchange (added by another exchange, e.g.) // they need to be added here SharedInstrumentInfo sharedInfo = getSharedInstrumentInfo(underlyingInstrument); assert(sharedInfo != null); Collection<FilteringSubscriber> interestedSubscribers = optionChainSubscribers.get(underlyingInstrument); for(final Instrument optionChainInstrument : sharedInfo.getOptionChain()) { updateInfo(new HasInstrument() { @Override public Instrument getInstrument() { return optionChainInstrument; } @Override public String getInstrumentAsString() { return optionChainInstrument.getSymbol(); } }); // make sure that any subscribers interested in the underlying instrument get the opportunity // to find out about what may be a new (to it) entry in the option chain if(interestedSubscribers != null) { for(FilteringSubscriber subscriber : interestedSubscribers) { subscriber.noticeOfOptionChainEntry(optionChainInstrument); } } } } } } /** * Executes all the ticks of the exchange script in <code>SCRIPTED</code> mode. * * @param inScriptedEvents a <code>List<QuoteEvent></code> value */ private void doScriptedTicks(List<QuoteEvent> inScriptedEvents) { for(QuoteEvent event : inScriptedEvents) { try { // verify the event is for this exchange if(!getCode().equals(event.getExchange())) { throw new IllegalArgumentException(SIMULATED_EXCHANGE_CODE_MISMATCH.getText(this, event, event.getExchange(), getCode())); } // prepare the exchange to handle the event's instrument updateInfo(event); // find the book that goes with the event PrivateInstrumentInfo book = getPrivateInstrumentInfo(event.getInstrument()); SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} executing scripted event {}", //$NON-NLS-1$ this, event); // process the event book.process(event); // settle the book as a result of this change Deque<MarketDataEvent> eventsToPublish = Lists.newLinkedList(settleBook(book)); // note that the events from processing the event and settling the book are all published in one // batch. this has functional implications because it means that several interim top-of-book states // may be compressed into one. this is the most correct behavior because an un-settled book // is a sub-atomic state publishEvents(eventsToPublish); } catch (Exception e) { SIMULATED_EXCHANGE_SKIPPED_EVENT.warn(SimulatedExchange.class, e, this, event); } } SIMULATED_EXCHANGE_OUT_OF_EVENTS.info(SimulatedExchange.class, getName()); } /** * Publishes the given events to interested subscribers. * * @param inEventsToPublish a <code>Deque<? extends Event></code> value */ private void publishEvents(Deque<? extends Event> inEventsToPublish) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} publishing events: {}", //$NON-NLS-1$ this, inEventsToPublish); Event lastEvent = inEventsToPublish.getLast(); if(lastEvent instanceof HasEventType) { ((HasEventType)lastEvent).setEventType(EventType.UPDATE_FINAL); } for(Event event : inEventsToPublish) { publisher.publish(event); } } /** * Executes one round of processing for all instruments. */ private void executeTick() { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} beginning tick {} at {}", //$NON-NLS-1$ this, iterationCounter.incrementAndGet(), DateUtils.dateToString(new Date())); // if the previous tick hasn't completed yet, skip this tick and wait for the next one if(readyForTick.getAndSet(false)) { // the previous tick has completed, so we can begin this one long startTime = System.currentTimeMillis(); try { for(PrivateInstrumentInfo book : books.values()) { doRandomBookTick(book); } } finally { // indicate that we're ready for the next tick readyForTick.set(true); SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} completed tick {} after {} ms", //$NON-NLS-1$ this, iterationCounter.get(), System.currentTimeMillis() - startTime); } } else { // the previous tick has not yet completed, skip this one SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} skipped tick {}", //$NON-NLS-1$ this, iterationCounter.get()); //$NON-NLS-1$ } } /** * Executes a single tick for the given order book. * * @param inBook an <code>OrderBookWrapper</code> value */ private void doRandomBookTick(PrivateInstrumentInfo inBook) { // adjust the order book base value inBook.adjustPrice(); // settle the book (generates additional activity which needs to be published) Deque<Event> eventsToPublish = Lists.newLinkedList(); eventsToPublish.addAll(settleBook(inBook)); // produce statistics eventsToPublish.addAll(getStatistics(ExchangeRequestBuilder.newRequest().withInstrument(inBook.getBook().getInstrument()) .withUnderlyingInstrument(inBook.getUnderlyingInstrument()).create())); if(inBook.getInstrument() instanceof Equity) { eventsToPublish.addAll(getDividends(ExchangeRequestBuilder.newRequest().withInstrument(inBook.getBook().getInstrument()).create())); } publishEvents(eventsToPublish); } /** * Determines the correct <code>ExpirationType</code> to use for the given <code>Option</code>. * * @param inOption an <code>Option</code> value * @return an <code>ExpirationType</code> */ private static ExpirationType getExpirationType(Option inOption) { // this is entirely an arbitrary choice: our exchanges deal with options with American-style expiration, apparently return ExpirationType.AMERICAN; } /** * Gets the <code>SharedInstrumentInfo</code> associated with the given * <code>Instrument</code>. * * <p>The <code>SharedInstrumentInfo</code> must already exist. It is the * caller's responsibility to make sure this is the case by making sure that * {@link #updateInfo(HasInstrument)} is called before invoking this method. * * @param inInstrument an <code>Instrument</code> value * @return a <code>SharedInstrumentInfo</code> value */ private static SharedInstrumentInfo getSharedInstrumentInfo(Instrument inInstrument) { SharedInstrumentInfo info = sharedInstruments.get(inInstrument); assert(info != null); return info; } /** * Updates the shared exchange information using the given <code>HasInstrument</code> value. * * <p>This method will set up the exchange to handle the given instrument. This method * may be called more than once with the same <code>HasInstrument</code> with no ill effect. * * @param inInstrumentProvider a <code>HasInstrument</code> value */ private static synchronized void updateSharedInfo(HasInstrument inInstrumentProvider) { // this method is synchronized because of the put-if-absent performed on sharedInstruments // figure out what information we have Instrument instrument = inInstrumentProvider.getInstrument(); Instrument underlyingInstrument = null; if(inInstrumentProvider instanceof HasUnderlyingInstrument) { HasUnderlyingInstrument underlyingInstrumentProvider = (HasUnderlyingInstrument)inInstrumentProvider; underlyingInstrument = underlyingInstrumentProvider.getUnderlyingInstrument(); } // check to see if we already know about this instrument if(instrument != null) { SharedInstrumentInfo info = sharedInstruments.get(instrument); if(info == null) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} is a new instrument, creating common info for it", //$NON-NLS-1$ instrument); // this instrument is new, update the shared info (underlyingInstrument may be null, that's OK) info = new SharedInstrumentInfo(instrument, underlyingInstrument); sharedInstruments.put(instrument, info); } // sharedInstruments is updated and info is non-null } // if an underlying instrument is present, get that info, too if(underlyingInstrument != null) { SharedInstrumentInfo underlyingInfo = sharedInstruments.get(underlyingInstrument); SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} has an underlying instrument {} and shared info {}", //$NON-NLS-1$ inInstrumentProvider, underlyingInstrument, underlyingInfo); // the presence of a non-null underlying instrument means that the primary instrument *must* be // an option (if specified) assert(instrument == null || instrument instanceof Option); if(underlyingInfo == null) { // the info for the underlying instrument doesn't exist yet - create it underlyingInfo = new SharedInstrumentInfo(underlyingInstrument, null); sharedInstruments.put(underlyingInstrument, underlyingInfo); SLF4JLoggerProxy.debug(SimulatedExchange.class, "Created new underlying info {}", //$NON-NLS-1$ underlyingInfo); } // if there's an instrument (which we now know is an option), add it to the underlying's // option chain (might already be there, but won't hurt to add it again) if(instrument != null) { underlyingInfo.addToOptionChain(instrument); SLF4JLoggerProxy.debug(SimulatedExchange.class, "Adding {} to option chain: {}", //$NON-NLS-1$ instrument, underlyingInfo); } } } /** * Examines the given <code>PrivateInstrumentInfo</code>, matching bids and asks * until all the bids are either filled or there are no matching asks. * * <p>Each bid is considered in turn, from the highest price to the lowest. * For each bid, the asks are examined in order from lowest to highest. * If the bid price is greater than or equal to the ask price, a trade * is created for the minimum value of the set of the bid size and ask * size. Both the bid and the ask are adjusted as appropriate. If the * bid is fully filled, it is removed from the book, otherwise, the ask * is removed and the next ask is considered. * * <p>This method requires exclusive access to the <code>PrivateInstrumentInfo</code> but * does not perform any synchronization explicitly. It is the caller's * responsibility to guarantee exclusive access to the <code>PrivateInstrumentInfo</code>. * * @param inBook a <code>PrivateInstrumentInfo</code> value * @return a <code>List<MarketDataEvent></code> value containing the events created, if any, to settle the book */ private static List<MarketDataEvent> settleBook(PrivateInstrumentInfo inBook) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "Settling book for {}: OrderBook starts at\n{}", //$NON-NLS-1$ inBook, inBook.getBook()); List<MarketDataEvent> eventsToReturn = new ArrayList<MarketDataEvent>(); try { // this is the list of bids over which to operate - note this is a static list, it does // not reflect the ongoing changes to the order book List<BidEvent> bids = new ArrayList<BidEvent>(inBook.getBook().getBidBook()); // this is the time that we're going to use for all the trades long tradeTime = System.currentTimeMillis(); // iterate over the bid list for(BidEvent bid : bids) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "Settler looking for matches for {}", //$NON-NLS-1$ bid); // search for the first ask that matches the bid BigDecimal bidPrice = bid.getPrice(); BigDecimal bidSize = bid.getSize(); // grab the list of asks, this, too, is a static list and is refreshed each bid iteration List<AskEvent> asks = new ArrayList<AskEvent>(inBook.getBook().getAskBook()); // iterate over the list of asks looking for a match for(AskEvent ask : asks) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "Bid has {} left to fill", //$NON-NLS-1$ bidSize); // check to see if the bid is fully filled before continuing if(bidSize.compareTo(BigDecimal.ZERO) != 1) { // bid is fully filled SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} fully filled", //$NON-NLS-1$ bid); break; // out of the ask iteration loop } // bid is not fully filled SLF4JLoggerProxy.debug(SimulatedExchange.class, "Examining {}", //$NON-NLS-1$ ask); BigDecimal askPrice = ask.getPrice(); // if the buyer is willing to pay at least as much as the seller will take (bid >= ask) if(bidPrice.compareTo(askPrice) != -1) { // hooray, we have a transaction BigDecimal askSize = ask.getSize(); // these values are important - they are used to create the trade and to adjust the bid and the ask // the price is the lower of what the buyer is willing to pay and what the seller will take BigDecimal tradePrice = bidPrice.min(askPrice); // the size is the lower of what the buyer wants and what the seller is willing to sell BigDecimal tradeSize = askSize; SLF4JLoggerProxy.debug(SimulatedExchange.class, "Trade is {} at {}", //$NON-NLS-1$ tradeSize.toPlainString(), tradePrice.toPlainString()); // create the new trade TradeEventBuilder<TradeEvent> tradeBuilder = TradeEventBuilder.tradeEvent(bid.getInstrument()).withEventType(EventType.UPDATE_PART) .withExchange(bid.getExchange()) .withPrice(tradePrice) .withSize(tradeSize) .withTradeDate(DateUtils.dateToString(new Date(tradeTime))); if(bid.getInstrument() instanceof Option) { tradeBuilder.withExpirationType(getExpirationType((Option)bid.getInstrument())); tradeBuilder.withUnderlyingInstrument(inBook.getUnderlyingInstrument()); } if(bid.getInstrument() instanceof Future) { tradeBuilder.withContractSize(100) .withDeliveryType(DeliveryType.PHYSICAL) .withStandardType(StandardType.STANDARD); } TradeEvent trade = tradeBuilder.create(); // these events are used to modify the orders in the book BidEvent bidCorrection; AskEvent askCorrection; if(tradeSize.compareTo(bidSize) == -1) { // trade is smaller than the bid, this is a partial fill bidCorrection = QuoteEventBuilder.change(bid, new Date(tradeTime), bidSize.subtract(tradeSize)); bidCorrection.setEventType(EventType.UPDATE_PART); askCorrection = QuoteEventBuilder.delete(ask); askCorrection.setEventType(EventType.UPDATE_PART); } else { // trade is equal to the bid, this is a full fill bidCorrection = QuoteEventBuilder.delete(bid); bidCorrection.setEventType(EventType.UPDATE_PART); askCorrection = tradeSize.equals(askSize) ? QuoteEventBuilder.delete(ask) : QuoteEventBuilder.change(ask, new Date(tradeTime), askSize.subtract(tradeSize)); askCorrection.setEventType(EventType.UPDATE_PART); } // adjust the remainder we need to fill bidSize = bidSize.subtract(tradeSize); SLF4JLoggerProxy.debug(SimulatedExchange.class, "OrderBookSettler is creating the following events:\n{}\n{}\n{}", //$NON-NLS-1$ trade, bidCorrection, askCorrection); // post events to the feed's internal book inBook.setLatestTrade(trade); inBook.getBook().process(bidCorrection); inBook.getBook().process(askCorrection); // collect the events to return to the subscribers eventsToReturn.add(trade); eventsToReturn.add(bidCorrection); eventsToReturn.add(askCorrection); } else { // all the rest of the asks are higher than the highest bid, so no point in looking any more SLF4JLoggerProxy.debug(SimulatedExchange.class, "Best Bid is less than Best Ask, quitting"); //$NON-NLS-1$ bids.clear(); asks.clear(); return eventsToReturn; } } asks.clear(); } bids.clear(); return eventsToReturn; } finally { SLF4JLoggerProxy.debug(SimulatedExchange.class, "Book settling complete for {}, OrderBook is now\n{}", //$NON-NLS-1$ inBook, inBook.getBook()); } } /** * Generates a random decimal value in the interval (-(inUpperBound-1).99,+(inUpperBound-1).99). * * @param inUpperBound an <code>int</code> value used to define the interval in which the returned value may occur * @return a <code>BigDecimal</code> value in the interval (-(inUpperBound-1).99,+(inUpperBound-1).99) */ private static BigDecimal randomDecimalDifference(int inUpperBound) { if(random.nextBoolean()) { // higher return BigDecimal.ZERO.add(randomDecimal(inUpperBound)); } else { // lower return BigDecimal.ZERO.subtract(randomDecimal(inUpperBound)); } } /** * Generates a random integer in the interval (0,inUpperBound]. * * @param inUpperBound an <code>int</code> value used to define the interval in which the returned value may occur * @return a <code>BigDecimal</code> value in the interval (0,inUpperBound] */ private static BigDecimal randomInteger(int inUpperBound) { return new BigDecimal(random.nextInt(inUpperBound)); } /** * Generates a random decimal value in the interval (0.00,(inUpperBound-1).99). * * @param inUpperBound an <code>int</code> value used to define the interval in which the returned value may occur * @return a <code>BigDecimal</code> value in the interval (0.00,(inUpperBound-1).99) */ private static BigDecimal randomDecimal(int inUpperBound) { return new BigDecimal(String.format("%s.%s", //$NON-NLS-1$ random.nextInt(inUpperBound), random.nextInt(100))); } // immutable state of this exchange /** * the name of this exchange */ private final String name; /** * the exchange code of this exchange */ private final String code; /** * the maximum depth for orderbooks held by this exchange */ private final int maxDepth; /** * indicates if the previous tick processing has completed or not */ private final AtomicBoolean readyForTick = new AtomicBoolean(true); /** * the order books for the instruments managed by this exchange */ private final Map<Instrument,PrivateInstrumentInfo> books = new ConcurrentHashMap<Instrument,PrivateInstrumentInfo>(); /** * counter used to identify ticks in {@link Status#RANDOM} mode */ private final AtomicLong iterationCounter = new AtomicLong(0); /** * counter used to identify market data requests, both synchronous and asynchronous */ private final AtomicLong requestCounter = new AtomicLong(0); /** * set of subscribers who are interested in the option chain of this object */ private final Multimap<Instrument,FilteringSubscriber> optionChainSubscribers; // mutable state of this exchange /** * the exchange status */ private volatile Status status; /** * stores the handle for the task submitted to the scheduler to run updates on this exchange */ private volatile ScheduledFuture<?> ticker = null; // common to all exchanges // immutable state of all exchanges /** * value used to add to or subtract from prices */ private static final BigDecimal PENNY = new BigDecimal("0.01"); //$NON-NLS-1$ /** * the number of milliseconds in one hour */ private static final long HOURms = 1000l * 60l * 60l; /** * the number of milliseconds in one day */ private static final long DAYms = HOURms * 24l; /** * random generator used to manipulate prices */ private static final Random random = new Random(System.nanoTime()); /** * data for instruments shared across exchanges */ private static final Map<Instrument,SharedInstrumentInfo> sharedInstruments = new ConcurrentHashMap<Instrument,SharedInstrumentInfo>(); /** * mechanism which manages the threads that create the market data */ private static final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); /** * publishes events generated by order books from exchanges and manages subscriptions */ private static final PublisherEngine publisher = new PublisherEngine(true); // inner classes /** * The status of the exchange. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $ * @since 1.5.0 */ @ClassVersion("$Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $") @ThreadSafe public static enum Status { /** * exchange is not running */ STOPPED, /** * exchange is running generating random data */ RANDOM, /** * exchange is running using scripted data */ SCRIPTED, /** * exchange is running using scripted data, and has completed its script */ COMPLETE; /** * Indicates if the exchange is running or not. * * @return a <code>boolean</code> value */ private boolean isRunning() { return this == RANDOM || this == SCRIPTED || this == COMPLETE; } } /** * Compares <code>Instrument</code> values in the context of their use * as underlying instruments and option chain instruments. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $ * @since 2.0.0 */ @ThreadSafe private static enum InstrumentComparator implements Comparator<Instrument> { INSTANCE; /* (non-Javadoc) * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) */ @Override public int compare(Instrument inO1, Instrument inO2) { // this comparator will be used for two different cases: // 1) comparing underlying instrument to underlying instrument (straight symbol compare with no tie-breaker) // 2) comparing option-chain entry to option-chain entry (symbol, expiry, strike, type in order) int result = inO1.getSymbol().compareTo(inO2.getSymbol()); if(result != 0) { return result; } if(inO1 instanceof Option && inO2 instanceof Option) { Option option1 = (Option)inO1; Option option2 = (Option)inO2; result = option1.getExpiry().compareTo(option2.getExpiry()); if(result != 0) { return result; } result = option1.getStrikePrice().compareTo(option2.getStrikePrice()); if(result != 0) { return result; } return option1.getType().compareTo(option2.getType()); } return 0; } } /** * Holds information specific to a particular exchange for a given <code>Instrument</code>. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $ * @since 2.0.0 */ @ThreadSafe @ClassVersion("$Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $") private class PrivateInstrumentInfo { /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("%s [latest=%s value=%s]", //$NON-NLS-1$ instrument, latestTrade, value); } /** * Gets the underlying instrument for this instrument, if any. * * @return an <code>Instrument</code> value or <code>null</code> */ private Instrument getUnderlyingInstrument() { return getSharedInstrumentInfo(getInstrument()).getUnderlyingInstrument(); } /** * Create a new PrivateInstrumentInfo instance. * * <p>The given instrument must exist in the shared exchange information. * * @param inInstrument an <code>Instrument</code> value */ private PrivateInstrumentInfo(Instrument inInstrument) { assert(inInstrument != null); instrument = inInstrument; latestTrade = null; SharedInstrumentInfo sharedInfo = getSharedInstrumentInfo(instrument); assert(sharedInfo != null); setValue(sharedInfo.getMostRecentValue()); book = new OrderBook(instrument, getMaxDepth()); } /** * Applies the changes implied by the given <code>Event</code> to * this book, if any. * * <p>If the event is not relevant to this book, this method does nothing. * * @param inEvent an <code>Event</code> value * @return a <code>Deque<Event></code> value containing the events * produced by the changes */ private Deque<Event> process(Event inEvent) { Deque<Event> newEvents = Lists.newLinkedList(); if(inEvent instanceof QuoteEvent) { QuoteEvent displacedEvent = book.process((QuoteEvent)inEvent); newEvents.add(inEvent); if(displacedEvent != null) { QuoteEvent deleteQuote = QuoteEventBuilder.delete(displacedEvent); deleteQuote.setEventType(EventType.UPDATE_PART); newEvents.add(deleteQuote); } } publishEvents(newEvents); SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} processed {} and produced for publication: {}", //$NON-NLS-1$ this, inEvent, newEvents); return newEvents; } /** * Adjust the price of the book and submit new events accordingly. * * <p>This method causes the price to be adjusted randomly. New bids and * asks are submitted to the object's order book. */ private void adjustPrice() { if(random.nextBoolean()) { value = value.add(PENNY); } else { if(!value.equals(PENNY)) { value = value.subtract(PENNY); } } // take the modified value and add a bid and an ask based on it Date timestamp = new Date(); Instrument marketInstrument = getBook().getInstrument(); // create an ask event builder QuoteEventBuilder<AskEvent> askBuilder = QuoteEventBuilder.askEvent(marketInstrument); askBuilder.withEventType(EventType.UPDATE_PART) .withExchange(getCode()) .withPrice(getValue().add(PENNY)) .withSize(randomInteger(10000)) .withQuoteDate(DateUtils.dateToString(timestamp)); // and a bid event builder QuoteEventBuilder<BidEvent> bidBuilder = QuoteEventBuilder.bidEvent(marketInstrument); bidBuilder.withEventType(EventType.UPDATE_PART) .withExchange(getCode()) .withPrice(getValue().subtract(PENNY)) .withSize(randomInteger(10000)) .withQuoteDate(DateUtils.dateToString(timestamp)); if(marketInstrument instanceof Option) { Instrument underlyingInstrument = getUnderlyingInstrument(); assert(underlyingInstrument != null); askBuilder.withExpirationType(getExpirationType((Option)marketInstrument)); bidBuilder.withExpirationType(getExpirationType((Option)marketInstrument)); askBuilder.withUnderlyingInstrument(underlyingInstrument); bidBuilder.withUnderlyingInstrument(underlyingInstrument); } if(marketInstrument instanceof Future) { askBuilder.withContractSize(100) .withDeliveryType(DeliveryType.PHYSICAL) .withStandardType(StandardType.STANDARD); bidBuilder.withContractSize(100) .withDeliveryType(DeliveryType.PHYSICAL) .withStandardType(StandardType.STANDARD); } // create the events process(askBuilder.create()); process(bidBuilder.create()); } /** * Get the book value. * * @return an <code>OrderBook</code> value */ private OrderBook getBook() { return book; } /** * Get the value value. * * @return a <code>BigDecimal</code> value */ private BigDecimal getValue() { return value; } /** * Sets the value value. * * @param a <code>BigDecimal</code> value */ private void setValue(BigDecimal inValue) { value = inValue; } /** * Get the latestTrade value. * * @return a <code>TradeEvent</code> value */ private TradeEvent getLatestTrade() { return latestTrade; } /** * Sets the latestTrade value. * * @param a <code>TradeEvent</code> value */ private void setLatestTrade(TradeEvent inLatestTrade) { latestTrade = inLatestTrade; } /** * Get the instrument value. * * @return a <code>Instrument</code> value */ private Instrument getInstrument() { return instrument; } // immutable state /** * the instrument of the book */ private final Instrument instrument; /** * the order book itself */ private final OrderBook book; // mutable state /** * the most recent value of the instrument */ private volatile BigDecimal value; /** * the most recent trade of the instrument */ private volatile TradeEvent latestTrade; } /** * Holds information common to all exchanges for a given <code>Instrument</code>. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $ * @since 2.0.0 */ @ThreadSafe @ClassVersion("$Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $") private static class SharedInstrumentInfo { /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("%s [value: %s underlying: %s option chain: %s]", //$NON-NLS-1$ instrument, mostRecentValue, underlyingInstrument, optionChain); } /** * Create a new SharedInstrumentInfo instance. * * @param inInstrument an <code>Instrument</code> value * @param inUnderlyingInstrument an <code>Instrument</code> value */ private SharedInstrumentInfo(Instrument inInstrument, Instrument inUnderlyingInstrument) { instrument = inInstrument; underlyingInstrument = inUnderlyingInstrument; setMostRecentValue(randomDecimal(100).add(PENNY)); // dividends may be issued for equities only List<DividendEvent> tempDividends = new ArrayList<DividendEvent>(); if(inInstrument instanceof Equity) { // there is always a current (most recent) dividend long timestamp = System.currentTimeMillis(); long oneDay = 1000 * 60 * 60 * 24; long oneQuarter = oneDay * 90; // approximate, not really important DividendEventBuilder builder = DividendEventBuilder.dividend().withEquity((Equity)inInstrument); tempDividends.add(builder.withAmount(randomDecimal(10).add(PENNY)) .withEventType(EventType.UPDATE_PART) .withCurrency("USD") //$NON-NLS-1$ .withDeclareDate(DateUtils.dateToString(new Date(timestamp - ((randomInteger(60).longValue() + 1) * oneDay)), DateUtils.DAYS)) .withExecutionDate(DateUtils.dateToString(new Date(timestamp - ((randomInteger(60).longValue() + 1) * oneDay)), DateUtils.DAYS)) .withFrequency(DividendFrequency.QUARTERLY) .withPaymentDate(DateUtils.dateToString(new Date(timestamp - ((randomInteger(60).longValue() + 1) * oneDay)), DateUtils.DAYS)) .withRecordDate(DateUtils.dateToString(new Date(timestamp - ((randomInteger(60).longValue() + 1) * oneDay)), DateUtils.DAYS)) .withStatus(DividendStatus.OFFICIAL) .withType(DividendType.CURRENT).create()); // that establishes the current dividend // now create, say, 3 more UNOFFICIAL future dividends for(long quarterCounter=1;quarterCounter<=3;quarterCounter++) { tempDividends.add(builder.withDeclareDate(null) .withEventType(EventType.UPDATE_PART) .withPaymentDate(null) .withRecordDate(null) .withExecutionDate(DateUtils.dateToString(new Date(timestamp + oneQuarter * quarterCounter), DateUtils.DAYS)) .withStatus(DividendStatus.UNOFFICIAL) .withType(DividendType.FUTURE).create()); } } dividends = ImmutableList.copyOf(tempDividends); } /** * Get the underlyingInstrument value. * * @return an <code>Instrument</code> value */ private Instrument getUnderlyingInstrument() { return underlyingInstrument; } /** * Get the instrument value. * * @return a <code>Instrument</code> value */ @SuppressWarnings("unused") // I know this isn't used, but it just feels wrong not to have the instrument on this object private Instrument getInstrument() { return instrument; } /** * Gets the current option chain for this instrument. * * @return a <code>SortedSet<Instrument></code> value (may be empty) */ private SortedSet<Instrument> getOptionChain() { return Collections.unmodifiableSortedSet(optionChain); } /** * Adds the given <code>Instrument</code> to this instrument's option chain. * * <p>Adding the same <code>Instrument</code> more than once has no effect. * * @param inInstrument an <code>Instrument</code> value */ private void addToOptionChain(Instrument inInstrument) { optionChain.add(inInstrument); } /** * Get the mostRecentValue value. * * @return a <code>BigDecimal</code> value */ private BigDecimal getMostRecentValue() { return mostRecentValue; } /** * Sets the mostRecentValue value. * * @param a <code>BigDecimal</code> value */ private void setMostRecentValue(BigDecimal inMostRecentValue) { mostRecentValue = inMostRecentValue; } /** * Gets the dividends for this <code>Instrument</code>, if any. * * @return a <code>List<DividendEvent></code> value */ private List<DividendEvent> getDividends() { return dividends; } /** * the instrument of the shared info */ private final Instrument instrument; /** * the underlying instrument of the shared info (may be null) */ private final Instrument underlyingInstrument; /** * most recent value seen at any exchange for the instrument */ private volatile BigDecimal mostRecentValue; /** * option chain of this instrument (may be empty) */ private final SortedSet<Instrument> optionChain = Collections.synchronizedSortedSet(new TreeSet<Instrument>(InstrumentComparator.INSTANCE)); /** * contains the dividends, if any, issued for this instrument */ private final List<DividendEvent> dividends; } /** * <code>ISubscriber</code> that filters publications to an enclosed <code>ISubscriber</code> * based on the original type of market data request and instrument. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $ * @since 1.5.0 */ @ThreadSafe @ClassVersion("$Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $") private static class FilteringSubscriber implements ISubscriber { /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("FilteringSubscriber for %s on %s watching %s", //$NON-NLS-1$ type, exchange, instruments); } /** * Subscribes the given <code>ISubscriber</code> to market data updates of the given * type for the given instrument. * * @param inOriginalSubscriber an <code>ISubscriber</code> value * @param inType a <code>Type</code> value * @param inInstruments a <code>Collection<Instrument></code> value * @param inExchange a <code>SimulatedExchange</code> value containing the owning exchange * @param inExchangeRequest an <code>ExchangeRequest</code> value containing the request * @return a <code>Token</code> value representing the subscription */ private static Token subscribe(ISubscriber inOriginalSubscriber, Type inType, Collection<Instrument> inInstruments, SimulatedExchange inExchange, ExchangeRequest inExchangeRequest) { FilteringSubscriber subscriber = new FilteringSubscriber(inOriginalSubscriber, inType, inInstruments, inExchange); // it's possible if this request is by underlying // instrument that entries will be added to the underlying instrument's option chain later. // results will be needed to be returned for these new entries, too. obviously, the books aren't // available before the symbol exists. there needs to be a way for this subscriber to be notified // when a new book is created for an option chain entry for the // set up the subscriber to be notified if new option chain entries are added for the underlying if(inExchangeRequest.isForUnderlyingOnly()) { inExchange.optionChainSubscribers.put(inExchangeRequest.getUnderlyingInstrument(), subscriber); } publisher.subscribe(subscriber); return subscriber.getToken(); } /** * Create a new FilteringSubscriber instance. * * @param inSubscriber an <code>ISubscriber</code> value * @param inType a <code>Type</code> value * @param inInstruments a <code>Collection<Instrument></code> value * @param inExchange a <code>SimulatedExchange</code> value containing the owning exchange */ private FilteringSubscriber(ISubscriber inSubscriber, Type inType, Collection<Instrument> inInstruments, SimulatedExchange inExchange) { originalSubscriber = inSubscriber; type = inType; instruments.addAll(inInstruments); token = new Token(this); exchange = inExchange; Multimap<Instrument,DividendEvent> dividends = HashMultimap.create(); lastKnownDividends = Multimaps.synchronizedMultimap(dividends); } /* (non-Javadoc) * @see org.marketcetera.core.publisher.ISubscriber#isInteresting(java.lang.Object) */ @Override public boolean isInteresting(Object inData) { // escape hatch for non-events if(!(inData instanceof Event)) { return true; } // verify the exchange matches if(inData instanceof MarketDataEvent) { if(!((MarketDataEvent)inData).getExchange().equals(exchange.getCode())) { return false; } } // verify the object has a relevant instrument (if it has has one) if(inData instanceof HasInstrument) { if(!instruments.contains(((HasInstrument)inData).getInstrument())) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} not interested in {}", //$NON-NLS-1$ this, inData); return false; } } // verify the object's type is relevant switch(type) { case TOP_OF_BOOK : return inData instanceof QuoteEvent; case LATEST_TICK : return inData instanceof TradeEvent; case DEPTH_OF_BOOK : return inData instanceof QuoteEvent; case STATISTICS : return inData instanceof MarketstatEvent; case DIVIDENDS : return inData instanceof DividendEvent; default : throw new UnsupportedOperationException(); } } /* (non-Javadoc) * @see org.marketcetera.core.publisher.ISubscriber#publishTo(java.lang.Object) */ @Override public synchronized void publishTo(Object inData) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "Subscriber {} received {} to examine", //$NON-NLS-1$ this, inData); if(type == Type.TOP_OF_BOOK) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} has subscribed to top-of-book - further analysis required", //$NON-NLS-1$ this, inData); // top-of-book is a special case. first, if we get this far, then the // class of inData *should* be QuoteEvent, but let's not bank on that if(inData instanceof QuoteEvent) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} is a quote, continuing", //$NON-NLS-1$ this); QuoteEvent quoteEvent = (QuoteEvent)inData; // a QuoteEvent for top-of-book is an opportunity for a new top-of-book state // for the exchange; an opportunity but *not* a guarantee // first, check to see if there is, in fact, a new top-of-book state for the // exchange TopOfBook newTopOfBook = null; ExchangeRequestBuilder topOfBookRequestBuilder = ExchangeRequestBuilder.newRequest().withInstrument(quoteEvent.getInstrument()); if(quoteEvent instanceof OptionEvent) { topOfBookRequestBuilder.withUnderlyingInstrument(((OptionEvent)quoteEvent).getUnderlyingInstrument()); } newTopOfBook = makeTopOfBook(exchange.getTopOfBook(topOfBookRequestBuilder.create())); SLF4JLoggerProxy.debug(SimulatedExchange.class, "New top-of-book is {}", //$NON-NLS-1$ newTopOfBook); TopOfBook lastKnownTopOfBook = lastKnownTopsOfBook.get(quoteEvent.getInstrument()); SLF4JLoggerProxy.debug(SimulatedExchange.class, "Last-known top-of-book is {}", //$NON-NLS-1$ lastKnownTopOfBook); // we are guaranteed that this object is non-null, but its components may be null // check to see if the quote event caused a change in the top-of-book state if(newTopOfBook.compareTo(lastKnownTopOfBook) != 0) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "New top-of-book is different from last-known top-of-book"); //$NON-NLS-1$ // *something* has changed in the top-of-book, but we don't know what yet // by design, top-of-book updates are sent as ADDs except if the quote is to be removed // (indicating there is no top-of-book) in which case the action will be DELETE // also, it's currently not possible for a single quote event to cause a change // in both sides of the book, but that doesn't mean it can't happen some time in // the future. it's an easy check to make, so go ahead and check both sides. note // that there's no guarantee that the object published to originalSubscriber is the // same object passed to this method, nor is there any kind of multiplicity guarantees // (can be one-for-one, many-for-one, none-for-one) // has the bid changed? publishCurrentSideIfNecessary(newTopOfBook.getBid(), (lastKnownTopOfBook == null ? null : lastKnownTopOfBook.getBid())); // has the ask changed? publishCurrentSideIfNecessary(newTopOfBook.getAsk(), (lastKnownTopOfBook == null ? null : lastKnownTopOfBook.getAsk())); } else { SLF4JLoggerProxy.debug(SimulatedExchange.class, "New top-of-book is *not* different from last-known top-of-book, nothing to do"); //$NON-NLS-1$ } lastKnownTopsOfBook.put(quoteEvent.getInstrument(), newTopOfBook); } return; } if(type == Type.DIVIDENDS) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} has subscribed to dividend - make sure that the dividend has not already been seen", //$NON-NLS-1$ this); if(inData instanceof DividendEvent) { DividendEvent dividendEvent = (DividendEvent)inData; Collection<DividendEvent> dividends = lastKnownDividends.get(dividendEvent.getEquity()); if(dividends != null && dividends.contains(dividendEvent)) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} has already seen {} - do nothing", //$NON-NLS-1$ this, dividendEvent); } else { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} has not yet seen {} - publish", //$NON-NLS-1$ this, dividendEvent); originalSubscriber.publishTo(inData); lastKnownDividends.put(dividendEvent.getEquity(), dividendEvent); } } return; } SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} has subscribed to something other than top-of-book or dividend - publish {}", //$NON-NLS-1$ this, inData); originalSubscriber.publishTo(inData); } /** * Notifies this publisher of an option chain entry for an underlying * instrument in which this publisher has expressed an interest. * * @param inOptionChainEntry an <code>Instrument</code> value */ private void noticeOfOptionChainEntry(Instrument inOptionChainEntry) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "{} received notification of a potential new option chain entry: {}", //$NON-NLS-1$ this, inOptionChainEntry); instruments.add(inOptionChainEntry); } /** * Publishes the side of the book implied by the type of the given <code>QuoteEvent</code>, * if necessary. * * @param inCurrentTop an <code>E</code> indicating the most recent top of the given side * @param inLastTop an <code>E</code> indicating the last known top of the given side. */ private <E extends QuoteEvent> void publishCurrentSideIfNecessary(E inCurrentTop, E inLastTop) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "Considering current {} and last {} to see if current needs to be published", //$NON-NLS-1$ inCurrentTop, inLastTop); if(inCurrentTop == null) { // there is no current top quote, was there one before? if(inLastTop != null) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "Last ({}) needs to be removed, publish as a delete", //$NON-NLS-1$ inLastTop); // yes, there used to be a top quote, but it should go away now QuoteEvent delete = QuoteEventBuilder.delete(inLastTop); delete.setEventType(EventType.UPDATE_PART); originalSubscriber.publishTo(delete); } else { // there didn't used to be a top quote, so don't do anything SLF4JLoggerProxy.debug(SimulatedExchange.class, "Both current and last are null - nothing to do"); //$NON-NLS-1$ } } else { // there is a current top quote, compare it to the one that used to be here if(inLastTop == null) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "Last is null, publish {}", //$NON-NLS-1$ inCurrentTop); // there didn't used to be a top quote, just add the new one QuoteEvent add = QuoteEventBuilder.add(inCurrentTop); add.setEventType(EventType.UPDATE_PART); originalSubscriber.publishTo(add); } else { // there used to be a top quote, check to see if it's different than the current one // btw, we know that current quote and last known quote are both non-null if(PriceAndSizeComparator.instance.compare(inLastTop, inCurrentTop) != 0) { SLF4JLoggerProxy.debug(SimulatedExchange.class, "Non-null current is different from non-null last: publish"); //$NON-NLS-1$ QuoteEvent add = QuoteEventBuilder.add(inCurrentTop); add.setEventType(EventType.UPDATE_PART); originalSubscriber.publishTo(add); } else { // the current and previous tops are identical, so don't do anything SLF4JLoggerProxy.debug(SimulatedExchange.class, "Current and last are non-null but identical: do not publish"); //$NON-NLS-1$ } } } } /** * Gets the <code>Token</code> corresponding to this subscription. * * @return a <code>Token</code> value */ private Token getToken() { return token; } /** * Creates a <code>TopOfBook</code> value from the given list of events, if possible. * * @param inEvents a <code>List<QuoteEvent></code> value * @return a <code>TopOfBook</code> value */ private static TopOfBook makeTopOfBook(List<QuoteEvent> inEvents) { // to make a top out of a list, the following must be true: // 1) the list contains 0, 1, or 2 QuoteEvents // 2) if 2, the first event must be a bid, the second an ask // 3) if 1, the event may be either a bid or an ask assert(inEvents.size() <= 2); if(inEvents.isEmpty()) { return new TopOfBook(null, null); } // event list contains 1 or 2 events, don't know what type yet QuoteEvent event1 = inEvents.remove(0); QuoteEvent event2; if(inEvents.isEmpty()) { event2 = null; } else { event2 = inEvents.remove(0); } assert(inEvents.isEmpty()); BidEvent bid = null; AskEvent ask = null; if(event1 instanceof BidEvent) { bid = (BidEvent)event1; if(event2 instanceof AskEvent) { ask = (AskEvent)event2; } } else if(event1 instanceof AskEvent) { ask = (AskEvent)event1; assert(event2 == null); } return new TopOfBook(bid, ask); } /** * the last known top of the relevant book or <code>null</code> */ private final Map<Instrument,TopOfBook> lastKnownTopsOfBook = new ConcurrentHashMap<Instrument,TopOfBook>(); /** * */ private final Multimap<Instrument,DividendEvent> lastKnownDividends; /** * the original (external to this class) subscriber */ private final ISubscriber originalSubscriber; /** * the type of request */ private final Type type; /** * the instrument for which the request was made */ private final Set<Instrument> instruments = Collections.synchronizedSet(new HashSet<Instrument>()); /** * the subscription token returned to the caller */ private final Token token; /** * the exchange to which this subscription was targeted */ private final SimulatedExchange exchange; } /** * Represents the top of a given book at a particular point in time. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $ * @since 2.0.0 */ @Immutable @ClassVersion("$Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $") static class TopOfBook extends Pair<BidEvent,AskEvent> implements Comparable<TopOfBook> { /** * Create a new TopOfBook instance. * * @param inBidEvent a <code>BidEvent</code> value or <code>null</code> * @param inAskEvent an <code>AskEvent</code> value or <code>null</code> * @throws IllegalArgumentException if both the <code>BidEvent</code> and <code>AskEvent</code> * are specified but are not events for the same <code>Instrument</code> */ TopOfBook(BidEvent inBidEvent, AskEvent inAskEvent) { super(inBidEvent, inAskEvent); if(inBidEvent != null && inAskEvent != null) { if(!inBidEvent.getInstrument().equals(inAskEvent.getInstrument())) { throw new IllegalArgumentException(); } } } /** * Gets the <code>BidEvent</code>. * * @return a <code>BidEvent</code> or <code>null</code> */ public BidEvent getBid() { return getFirstMember(); } /** * Gets the <code>AskEvent</code>. * * @return an <code>AskEvent</code> or <code>null</code> */ public AskEvent getAsk() { return getSecondMember(); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("%s -- %s", //$NON-NLS-1$ getBid(), getAsk()); } /* (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) */ @Override public int compareTo(TopOfBook inOtherTop) { if(inOtherTop == null) { return -1; } if(getBid() == null) { // top1 bid is null if(inOtherTop.getBid() != null) { // top2 bid is non-null return 1; } // top1 bid is null and top2 bid is null } else { // top1 bid is non-null if(inOtherTop.getBid() == null) { // top1 bid is non-null and top2 bid is null return -1; } // top1 bid is non-null and top2 bid is non-null int result = getBid().getPrice().compareTo(inOtherTop.getBid().getPrice()); if(result != 0) { return result; } result = getBid().getSize().compareTo(inOtherTop.getBid().getSize()); if(result != 0) { return result; } } // top1 bid is equal (in size and price) to top2 bid and both are non-null, move on to asks if(getAsk() == null) { // top1 ask is null if(inOtherTop.getAsk() != null) { // top2 ask is non-null return 1; } // top1 ask is null and top2 ask is null } else { // top1 ask is non-null if(inOtherTop.getAsk() == null) { // top1 ask is non-null and top2 ask is null return -1; } // top1 ask is non-null and top2 ask is non-null int result = getAsk().getPrice().compareTo(inOtherTop.getAsk().getPrice()); if(result != 0) { return result; } result = getAsk().getSize().compareTo(inOtherTop.getAsk().getSize()); if(result != 0) { return result; } } return 0; } } /** * Unique identifier for a specific subscription request to the {@link SimulatedExchange}. * * <p>This object is used to identify a market data request. When * executing a subscription request, as in to * {@link Exchange#getDepthOfBook(ExchangeRequest, ISubscriber)} * for instance, a <code>Token</code> value will be returned. Updates will be published to the * given {@link ISubscriber} until the exchange is stopped or the request is canceled via * {@link Token#cancel()}. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $ * @since 1.5.0 */ @ClassVersion("$Id: SimulatedExchange.java 16912 2014-05-16 23:35:10Z colin $") public static final class Token { /** * the original requester of the data */ private final FilteringSubscriber subscriber; /** * Create a new Token instance. * @param inSubscriber an <code>ISubscriber</code> value */ private Token(FilteringSubscriber inSubscriber) { subscriber = inSubscriber; } /** * Gets the subscriber who requested the data. * * @return a <code>FilteringSubscriber</code> value */ private FilteringSubscriber getSubscriber() { return subscriber; } /** * Cancels the subscription represented by this request. */ public void cancel() { subscriber.exchange.cancel(this); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("Simulated Exchange Token for %s", //$NON-NLS-1$ subscriber); } } }