package org.marketcetera.marketdata.bogus; import static org.marketcetera.marketdata.AssetClass.CONVERTIBLE_BOND; import static org.marketcetera.marketdata.AssetClass.CURRENCY; import static org.marketcetera.marketdata.AssetClass.EQUITY; import static org.marketcetera.marketdata.AssetClass.FUTURE; import static org.marketcetera.marketdata.AssetClass.OPTION; import static org.marketcetera.marketdata.Capability.DIVIDEND; import static org.marketcetera.marketdata.Capability.EVENT_BOUNDARY; import static org.marketcetera.marketdata.Capability.LATEST_TICK; import static org.marketcetera.marketdata.Capability.LEVEL_2; import static org.marketcetera.marketdata.Capability.MARKET_STAT; import static org.marketcetera.marketdata.Capability.OPEN_BOOK; import static org.marketcetera.marketdata.Capability.TOP_OF_BOOK; import static org.marketcetera.marketdata.Capability.TOTAL_VIEW; import static org.marketcetera.marketdata.bogus.Messages.UNSUPPORTED_OPTION_SPECIFICATION; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import org.marketcetera.core.NoMoreIDsException; import org.marketcetera.core.publisher.ISubscriber; import org.marketcetera.event.Event; import org.marketcetera.marketdata.AbstractMarketDataFeed; import org.marketcetera.marketdata.AssetClass; import org.marketcetera.marketdata.Capability; import org.marketcetera.marketdata.Content; import org.marketcetera.marketdata.ExchangeRequest; import org.marketcetera.marketdata.ExchangeRequestBuilder; import org.marketcetera.marketdata.FeedException; import org.marketcetera.marketdata.MarketDataFeed; import org.marketcetera.marketdata.MarketDataFeedTokenSpec; import org.marketcetera.marketdata.MarketDataRequest; import org.marketcetera.marketdata.SimulatedExchange; import org.marketcetera.options.OptionUtils; import org.marketcetera.trade.ConvertibleBond; import org.marketcetera.trade.Currency; import org.marketcetera.trade.Equity; import org.marketcetera.trade.Future; import org.marketcetera.trade.Instrument; import org.marketcetera.trade.Option; import org.marketcetera.util.log.I18NBoundMessage1P; import org.marketcetera.util.log.SLF4JLoggerProxy; import org.marketcetera.util.misc.ClassVersion; /* $License$ */ /** * Sample implementation of {@link MarketDataFeed}. * * <p>This implementation generates random market data for each * symbol for which a market data request is received. Data is returned * from the feed via {@link Event} objects. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: BogusFeed.java 16912 2014-05-16 23:35:10Z colin $ * @since 0.5.0 */ @ClassVersion("$Id: BogusFeed.java 16912 2014-05-16 23:35:10Z colin $") public class BogusFeed extends AbstractMarketDataFeed<BogusFeedToken, BogusFeedCredentials, BogusFeedMessageTranslator, BogusFeedEventTranslator, MarketDataRequest, BogusFeed> { /** * Returns an instance of <code>BogusFeed</code>. * * @param inProviderName a <code>String</code> value * @return a <code>BogusFeed</code> value * @throws NoMoreIDsException if a unique identifier could not be generated to * be assigned */ public synchronized static BogusFeed getInstance(String inProviderName) throws NoMoreIDsException { if(sInstance != null) { return sInstance; } sInstance = new BogusFeed(inProviderName); return sInstance; } /** * Create a new BogusFeed instance. * * @param inProviderName a <code>String</code> value * @throws NoMoreIDsException if a unique identifier could not be generated to * be assigned */ private BogusFeed(String inProviderName) throws NoMoreIDsException { super(FeedType.SIMULATED, inProviderName); setLoggedIn(false); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#start() */ @Override public synchronized void start() { if(getFeedStatus().isRunning()) { throw new IllegalStateException(); } for(int i=0;i<EXCHANGE_COUNT;i++) { SimulatedExchange exchange = new SimulatedExchange(String.format("%s-%d", //$NON-NLS-1$ getProviderName(), i+1), String.format("BGS%d", //$NON-NLS-1$ i+1)); SLF4JLoggerProxy.debug(BogusFeed.class, "BogusFeed starting exchange {}...", //$NON-NLS-1$ exchange.getCode()); exchange.start(); exchanges.put(exchange.getCode(), exchange); } super.start(); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#stop() */ @Override public synchronized void stop() { if(!getFeedStatus().isRunning()) { throw new IllegalStateException(); } for(SimulatedExchange exchange : exchanges.values()) { SLF4JLoggerProxy.debug(BogusFeed.class, "BogusFeed stopping exchange {}...", //$NON-NLS-1$ exchange.getCode()); exchange.stop(); } super.stop(); } /* (non-Javadoc) * @see org.marketcetera.marketdata.MarketDataFeed#getCapabilities() */ @Override public Set<Capability> getCapabilities() { return capabilities; } /* (non-Javadoc) * @see org.marketcetera.marketdata.MarketDataFeed#getSupportedAssetClasses() */ @Override public Set<AssetClass> getSupportedAssetClasses() { return assetClasses; } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#doCancel(java.lang.String) */ @Override protected final synchronized void doCancel(String inHandle) { Request.cancel(inHandle); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#doLevelOneMarketDataRequest(java.lang.Object) */ @Override protected final synchronized List<String> doMarketDataRequest(MarketDataRequest inData) throws FeedException { return Arrays.asList(Request.execute(inData, this)); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#doLogin(org.marketcetera.marketdata.AbstractMarketDataFeedCredentials) */ @Override protected final boolean doLogin(BogusFeedCredentials inCredentials) { setLoggedIn(true); return true; } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#doLogout() */ @Override protected final void doLogout() { setLoggedIn(false); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#generateToken(quickfix.Message) */ @Override protected final BogusFeedToken generateToken(MarketDataFeedTokenSpec inTokenSpec) throws FeedException { return BogusFeedToken.getToken(inTokenSpec, this); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#getEventTranslator() */ @Override protected final BogusFeedEventTranslator getEventTranslator() { return BogusFeedEventTranslator.getInstance(); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#getMessageTranslator() */ @Override protected final BogusFeedMessageTranslator getMessageTranslator() { return BogusFeedMessageTranslator.getInstance(); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#isLoggedIn(org.marketcetera.marketdata.AbstractMarketDataFeedCredentials) */ @Override protected final boolean isLoggedIn() { return mLoggedIn; } /** * Sets the loggedIn value. * * @param inLoggedIn Logged-in status of the feed */ private void setLoggedIn(boolean inLoggedIn) { mLoggedIn = inLoggedIn; } /** * Gets the list of exchanges associated with the given exchange code. * * @param inExchange a <code>String</code> value containing an exchange code or null to return * all exchanges * @return a <code>List<SimulatedExchange></code> value */ private List<SimulatedExchange> getExchangesForCode(String inExchange) { if(inExchange == null || inExchange.isEmpty()) { // request data from all exchanges return new ArrayList<SimulatedExchange>(exchanges.values()); } if(exchanges.containsKey(inExchange)) { return Arrays.asList(new SimulatedExchange[] { exchanges.get(inExchange) }); } return new ArrayList<SimulatedExchange>(); } /** * capabilities for BogusFeed - note that these are not dynamic as Bogus requires no provisioning */ private static final Set<Capability> capabilities = Collections.unmodifiableSet(EnumSet.of(TOP_OF_BOOK,LEVEL_2,OPEN_BOOK,TOTAL_VIEW,LATEST_TICK,MARKET_STAT,DIVIDEND,EVENT_BOUNDARY)); /** * supported asset classes */ private static final Set<AssetClass> assetClasses = EnumSet.of(EQUITY,OPTION,FUTURE,CURRENCY,CONVERTIBLE_BOND); /** * indicates if the feed has been logged in to */ private boolean mLoggedIn; /** * exchanges that make up the group for which the bogus feed can report data */ private final Map<String,SimulatedExchange> exchanges = new HashMap<String,SimulatedExchange>(); /** * static instance of <code>BogusFeed</code> */ private static BogusFeed sInstance; /** * arbitrarily chosen number of internal exchanges to aggregate */ private static final int EXCHANGE_COUNT = 1; /** * Corresponds to a single market data request submitted to {@link BogusFeed}. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: BogusFeed.java 16912 2014-05-16 23:35:10Z colin $ * @since 1.5.0 */ @ClassVersion("$Id: BogusFeed.java 16912 2014-05-16 23:35:10Z colin $") private static class Request { /** * Executes the given <code>MarketDataRequest</code> and returns * a handle corresponding to the request. * * @param inRequest a <code>MarketDataRequest</code> value * @param inParentFeed a <code>BogusFeed</code> value * @return a <code>String</code> value * @throws FeedException if the request could not be executed */ private static String execute(MarketDataRequest inRequest, BogusFeed inParentFeed) throws FeedException { Request request = new Request(inRequest, inParentFeed); request.execute(); return request.getIDAsString(); } /** * Cancels the market data request associated with the given handle. * * @param inHandle a <code>String</code> value */ private static void cancel(String inHandle) { Request request; synchronized(requests) { request = requests.remove(inHandle); } if(request != null) { request.cancel(); } } /** * Create a new Request instance. * * @param inRequest a <code>MarketDataRequest</code> value * @param inFeed a <code>BogusFeed</code> value */ private Request(MarketDataRequest inRequest, BogusFeed inFeed) { marketDataRequest = inRequest; feed = inFeed; subscriber = new ISubscriber() { @Override public boolean isInteresting(Object inData) { return true; } @Override public void publishTo(Object inData) { SLF4JLoggerProxy.debug(BogusFeed.class, "BogusFeed publishing {}", //$NON-NLS-1$ inData); feed.dataReceived(getIDAsString(), inData); } }; synchronized(requests) { requests.put(getIDAsString(), this); } } /** * Gets the underlying instrument for the given symbol. * * @param inSymbol a <code>String</code> value * @return an <code>Instrument</code> value */ private Instrument getUnderlyingInstrument(String inSymbol) { return new Equity(inSymbol); // this is slightly restrictive in the long run, but certainly acceptable for now } /** * Executes the market data request associated with this object. * * @throws IllegalStateException if this method has already been executed for this object * @throws FeedException if the request is for Option asset class and contains a symbol that is not OSI-compliant */ private synchronized void execute() throws FeedException { if(executed) { throw new IllegalStateException(); } try { List<ExchangeRequest> exchangeRequests = new ArrayList<ExchangeRequest>(); Set<String> symbols = marketDataRequest.getSymbols(); if(!symbols.isEmpty()) { if(marketDataRequest.getAssetClass() == AssetClass.EQUITY) { for(String symbol : symbols) { exchangeRequests.add(ExchangeRequestBuilder.newRequest().withInstrument(new Equity(symbol)).create()); } } else if(marketDataRequest.getAssetClass() == AssetClass.FUTURE) { for(String symbol : symbols) { exchangeRequests.add(ExchangeRequestBuilder.newRequest().withInstrument(Future.fromString(symbol)).create()); } } else if(marketDataRequest.getAssetClass() == AssetClass.CURRENCY) { for(String symbol : symbols) { exchangeRequests.add(ExchangeRequestBuilder.newRequest().withInstrument(new Currency(symbol)).create()); } } else if(marketDataRequest.getAssetClass() == AssetClass.OPTION) { for(String symbol : symbols) { // this assumes that the symbol is an OSI-compliant symbol, otherwise, there's no way to parse it // deterministically. if it's not OSI-compliant, an IAE will be thrown, which puts the kaibosh on // the whole request. this is a limitation ironed into the Bogus adapter try { Option basicOption = OptionUtils.getOsiOptionFromString(symbol); exchangeRequests.add(ExchangeRequestBuilder.newRequest().withInstrument(basicOption) .withUnderlyingInstrument(getUnderlyingInstrument(basicOption.getSymbol())).create()); } catch (IllegalArgumentException e) { throw new FeedException(e, new I18NBoundMessage1P(UNSUPPORTED_OPTION_SPECIFICATION, symbol)); } } } else if(marketDataRequest.getAssetClass() == AssetClass.CONVERTIBLE_BOND) { for(String symbol : symbols) { exchangeRequests.add(ExchangeRequestBuilder.newRequest().withInstrument(new ConvertibleBond(symbol)).create()); } } else { // this is a new asset class and there is no support for it here throw new UnsupportedOperationException(); } } else { // marketDataRequest has no symbols - should then have underlyingsymbols instead Set<String> underlyingSymbols = marketDataRequest.getUnderlyingSymbols(); assert(!underlyingSymbols.isEmpty()); for(String symbol : marketDataRequest.getUnderlyingSymbols()) { exchangeRequests.add(ExchangeRequestBuilder.newRequest().withUnderlyingInstrument(getUnderlyingInstrument(symbol)).create()); } } for(ExchangeRequest exchangeRequest : exchangeRequests) { // all symbols for which we want data are collected in the symbols list // each type of subscription is managed differently for(Content content : marketDataRequest.getContent()) { switch(content) { case LEVEL_2 : // LEVEL_2 is NASDAQ Level II data, which is TOP_OF_BOOK from all // managed exchanges. doTopOfBook(exchangeRequest, null); break; case TOP_OF_BOOK : // TOP_OF_BOOK from the specified exchange only doTopOfBook(exchangeRequest, marketDataRequest.getExchange()); break; case OPEN_BOOK : // OPEN_BOOK is depth-of-book from the specified exchange doDepthOfBook(exchangeRequest, marketDataRequest.getExchange()); break; case TOTAL_VIEW : // TOTAL_VIEW is depth-of-book from the specified exchange doDepthOfBook(exchangeRequest, marketDataRequest.getExchange()); break; case LATEST_TICK : // LATEST_TICK is the most recent trade doLatestTick(exchangeRequest, marketDataRequest.getExchange()); break; case MARKET_STAT : doStatistics(exchangeRequest, marketDataRequest.getExchange()); break; case DIVIDEND : doDividends(exchangeRequest, marketDataRequest.getExchange()); break; default: throw new UnsupportedOperationException(); } } } } finally { executed = true; } } /** * Executes a statistics request for the given request using the * given exchange code. * * @param inExchangeRequest an <code>ExchangeRequest</code> value * @param inExchangeToUse a <code>String</code> value */ private void doStatistics(ExchangeRequest inExchangeRequest, String inExchangeToUse) { for(SimulatedExchange exchange : feed.getExchangesForCode(inExchangeToUse)) { exchangeTokens.add(exchange.getStatistics(inExchangeRequest, subscriber)); } } /** * Executes a dividend request for the given request using the * given exchange code. * * @param inExchangeRequest an <code>ExchangeRequest</code> value * @param inExchangeToUse a <code>String</code> value */ private void doDividends(ExchangeRequest inExchangeRequest, String inExchangeToUse) { for(SimulatedExchange exchange : feed.getExchangesForCode(inExchangeToUse)) { exchangeTokens.add(exchange.getDividends(inExchangeRequest, subscriber)); // dividends are the same across all feeds, so the first answer is sufficient break; } } /** * Executes a depth-of-book request for the given request using the * given exchange code. * * @param inRequest an <code>ExchangeRequest</code> value * @param inExchangeToUse a <code>String</code> value */ private void doDepthOfBook(ExchangeRequest inRequest, String inExchangeToUse) { for(SimulatedExchange exchange : feed.getExchangesForCode(inExchangeToUse)) { exchangeTokens.add(exchange.getDepthOfBook(inRequest, subscriber)); } } /** * Executes a top-of-book request for the given request using the * given exchange code. * * @param inRequest an <code>ExchangeRequest</code> value * @param inExchangeToUse a <code>String</code> value */ private void doTopOfBook(ExchangeRequest inRequest, String inExchangeToUse) { for(SimulatedExchange exchange : feed.getExchangesForCode(inExchangeToUse)) { exchangeTokens.add(exchange.getTopOfBook(inRequest, subscriber)); } } /** * Executes a latest-tick request for the given request using the given * exchange code. * * @param inRequest an <code>ExchangeRequest</code> value * @param inExchangeToUse a <code>String</code> value */ private void doLatestTick(ExchangeRequest inRequest, String inExchangeToUse) { for(SimulatedExchange exchange : feed.getExchangesForCode(inExchangeToUse)) { exchangeTokens.add(exchange.getLatestTick(inRequest, subscriber)); } } /** * Cancels all subscriptions associated with this request. */ private void cancel() { for(SimulatedExchange.Token token : exchangeTokens) { token.cancel(); } } /** * Returns the request ID as a <code>String</code>. * * @return a <code>String</code> value */ private String getIDAsString() { return Long.toHexString(id); } /** * the collection of subscription tokens associated with this request */ private final List<SimulatedExchange.Token> exchangeTokens = new ArrayList<SimulatedExchange.Token>(); /** * the market data request associated with this object */ private final MarketDataRequest marketDataRequest; /** * the unique identifier of this request */ private final long id = counter.incrementAndGet(); /** * the parent object for this request */ private final BogusFeed feed; /** * the bridge object which receives responses from the parent's nested exchanges * and forwards them to the submitter of the request */ private final ISubscriber subscriber; /** * indicates whether this object has been executed yet or not */ private boolean executed = false; /** * all requests by their ID (represented as string) */ private static final Map<String,Request> requests = new HashMap<String,Request>(); /** * counter used to generate unique ids */ private static final AtomicLong counter = new AtomicLong(0); } }