package org.marketcetera.marketdata.marketcetera; 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 java.io.File; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; 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.WeakHashMap; import java.util.concurrent.Exchanger; import java.util.concurrent.TimeUnit; import org.marketcetera.core.ClassVersion; import org.marketcetera.core.CoreException; import org.marketcetera.core.IDFactory; import org.marketcetera.core.InMemoryIDFactory; import org.marketcetera.core.NoMoreIDsException; import org.marketcetera.marketdata.AbstractMarketDataFeed; import org.marketcetera.marketdata.AssetClass; import org.marketcetera.marketdata.Capability; import org.marketcetera.marketdata.FIXCorrelationFieldSubscription; import org.marketcetera.marketdata.FeedException; import org.marketcetera.marketdata.FeedStatus; import org.marketcetera.marketdata.MarketDataFeedTokenSpec; import org.marketcetera.marketdata.MarketDataRequest; import org.marketcetera.quickfix.EventLogFactory; import org.marketcetera.quickfix.FIXDataDictionary; import org.marketcetera.quickfix.FIXMessageUtil; import org.marketcetera.quickfix.FIXVersion; import org.marketcetera.trade.Equity; import org.marketcetera.util.log.SLF4JLoggerProxy; import quickfix.Application; import quickfix.DoNotSend; import quickfix.FieldNotFound; import quickfix.FileLogFactory; import quickfix.IncorrectDataFormat; import quickfix.IncorrectTagValue; import quickfix.Initiator; import quickfix.LogFactory; import quickfix.MemoryStoreFactory; import quickfix.Message; import quickfix.Message.Header; import quickfix.MessageStoreFactory; import quickfix.RejectLogon; import quickfix.Session; import quickfix.SessionID; import quickfix.SessionNotFound; import quickfix.SessionSettings; import quickfix.SocketInitiator; import quickfix.StringField; import quickfix.UnsupportedMessageType; import quickfix.field.MarketDepth; import quickfix.field.MsgType; import quickfix.field.NoMDEntryTypes; import quickfix.field.NoRelatedSym; import quickfix.field.SubscriptionRequestType; import quickfix.field.Symbol; import quickfix.fix44.MessageFactory; import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; /* $License$ */ /** * A sample implementation of a market data feed. * * <p>This feed will return random market data for every symbol queried. * * @author <a href="mailto:colin@marketcetera.com>Colin DuPlantis</a> * @since 0.5.0 */ @ClassVersion("$Id: MarketceteraFeed.java 16893 2014-04-25 18:20:56Z colin $") public class MarketceteraFeed extends AbstractMarketDataFeed<MarketceteraFeedToken, MarketceteraFeedCredentials, MarketceteraFeedMessageTranslator, MarketceteraFeedEventTranslator, MarketceteraFeed.Request, MarketceteraFeed> implements Application, Messages { private SessionID sessionID; private final IDFactory idFactory; private boolean isRunning = false; private SocketInitiator socketInitiator; private MessageFactory messageFactory; private final Map<String, Exchanger<Message>> pendingRequests = new WeakHashMap<String, Exchanger<Message>>(); private MarketceteraFeedCredentials credentials; /** * static capabilities for this data feed */ private static final Set<Capability> capabilities = Collections.unmodifiableSet(EnumSet.of(Capability.TOP_OF_BOOK,Capability.LATEST_TICK,Capability.MARKET_STAT)); /** * static supported asset classes for this data feed */ private static final Set<AssetClass> assetClasses = Collections.unmodifiableSet(EnumSet.of(EQUITY,OPTION,FUTURE,CURRENCY)); /* (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; } private FIXCorrelationFieldSubscription doQuery(Message query) { try { Integer marketDepth = null; try { marketDepth = query.getInt(MarketDepth.FIELD); } catch (FieldNotFound fnf) { // do nothing, this is OK, not every query has to have a depth } String reqID = addReqID(query); sendMessage(query); return new FIXCorrelationFieldSubscription(reqID, query.getHeader().getString(MsgType.FIELD), marketDepth); } catch (SessionNotFound e) { SESSION_NOT_FOUND.error(this, e); } catch (FieldNotFound e) { CANNOT_EXECUTE_QUERY.error(this, e, query); } return null; } private String getReqID(Message inMessage) { String reqID = null; try { String msgType = inMessage.getHeader().getString(MsgType.FIELD); StringField reqIDField = FIXMessageUtil.getCorrelationField(FIXVersion.FIX44, msgType); reqID = inMessage.getField(reqIDField).getValue(); } catch (FieldNotFound e) { CANNOT_FIND_REQID.error(this, e, inMessage); } return reqID; } private String addReqID(Message query) throws FieldNotFound { String reqID = getReqID(query); String msgType = query.getHeader().getString(MsgType.FIELD); StringField reqIDField = FIXMessageUtil.getCorrelationField(FIXVersion.FIX44, msgType); try { query.getField(reqIDField).toString(); } catch (FieldNotFound e1) { CANNOT_FIND_REQID.error(this, e1, query); } if (reqIDField.getValue() == null || reqIDField.getValue().length()==0){ try { reqID = idFactory.getNext(); } catch (NoMoreIDsException e) { // should never happen CANNOT_ACQUIRE_ID.error(this, e); assert(false); } reqIDField.setValue(reqID); query.setField(reqIDField); } return reqID; } private void sendMessage(Message message) throws SessionNotFound { Session.sendToTarget(message, sessionID); } public Equity symbolFromString(String symbolString) { if (MarketceteraOptionSymbol.matchesPattern(symbolString)){ return new MarketceteraOptionSymbol(symbolString); } return new Equity(symbolString); } public boolean isRunning() { return isRunning; } private void setIsRunning(boolean inIsRunning) { isRunning = inIsRunning; } /** * Creates an active connection to the Marketcetera Exchange server. * * <p>Attempts to connect to the server with the most recent set of credentials available. If there * is already an active connection, this method does nothing. This method will block for 30 seconds * while waiting for confirmation from the server. If at the end of 30 seconds the server has not * responded, this method throws a <code>FeedException</code>. This method updates the feed status * based on the results of the connection attempt. * * @throws FeedException if a connection cannot be made to the server */ private void connectToServer() throws Exception { SLF4JLoggerProxy.debug(this, "Checking connection to Marketcetera Feed"); //$NON-NLS-1$ if(isRunning()) { SLF4JLoggerProxy.debug(this, "Already connected to Marketcetera Feed"); //$NON-NLS-1$ return; } if(credentials == null) { SLF4JLoggerProxy.debug(this, "No credentials to work with, cancelling connection request - try again later"); //$NON-NLS-1$ } SLF4JLoggerProxy.debug(this, "Not connected yet, connecting with credentials [{}]...", //$NON-NLS-1$ credentials); String url = credentials.getURL(); URI feedURI = new URI(url); int serverPort = feedURI.getPort(); if ((serverPort) < 0){ URI_MISSING_PORT.error(AbstractMarketDataFeed.DATAFEED_STATUS_MESSAGES); throw new FeedException(URI_MISSING_PORT); } String server = feedURI.getHost(); String senderCompID = credentials.getSenderCompID(); if (senderCompID == null || senderCompID.trim().isEmpty()) { senderCompID = idFactory.getNext(); } String targetCompID = credentials.getTargetCompID(); String scheme; if (!FIXDataDictionary.FIX_4_4_BEGIN_STRING.equals(scheme = feedURI.getScheme()) ) { UNSUPPORTED_FIX_VERSION.error(AbstractMarketDataFeed.DATAFEED_STATUS_MESSAGES); throw new CoreException(UNSUPPORTED_FIX_VERSION); } else { sessionID = new SessionID(scheme, senderCompID, targetCompID); } synchronized(this) { try { setFeedStatus(FeedStatus.OFFLINE); CONNECTION_STARTED.info(this, url); MessageStoreFactory messageStoreFactory = new MemoryStoreFactory(); SessionSettings sessionSettings; sessionSettings = new SessionSettings(MarketceteraFeed.class.getClassLoader().getResourceAsStream("fixdatafeed.properties")); //$NON-NLS-1$ sessionSettings.setString(sessionID, Initiator.SETTING_SOCKET_CONNECT_HOST, server); sessionSettings.setLong(sessionID, Initiator.SETTING_SOCKET_CONNECT_PORT, serverPort); File workspaceDir = new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$ File quoteFeedLogDir = new File(workspaceDir, "marketdata"); //$NON-NLS-1$ if (!quoteFeedLogDir.exists()) { quoteFeedLogDir.mkdir(); } sessionSettings.setString(sessionID, FileLogFactory.SETTING_FILE_LOG_PATH, quoteFeedLogDir.getCanonicalPath()); LogFactory logFactory = new EventLogFactory(sessionSettings); messageFactory = new MessageFactory(); socketInitiator = new SocketInitiator(this, messageStoreFactory, sessionSettings, logFactory, messageFactory); socketInitiator.start(); SLF4JLoggerProxy.debug(this, "Connected, waiting for confirmation"); //$NON-NLS-1$ wait(1000*30); if(!getFeedStatus().equals(FeedStatus.AVAILABLE)) { throw new FeedException(CANNOT_START_FEED); } setIsRunning(true); SLF4JLoggerProxy.debug(this, "Connection confirmed, ready to proceed"); //$NON-NLS-1$ } catch (Exception e) { SLF4JLoggerProxy.debug(this, "Connection attempt failed!"); //$NON-NLS-1$ CANNOT_START_FEED.error(AbstractMarketDataFeed.DATAFEED_STATUS_MESSAGES, e); setFeedStatus(FeedStatus.ERROR); throw e; } } } public void stop() { synchronized(this) { if (isRunning()) { CONNECTION_STOPPED.info(this, credentials.getURL()); socketInitiator.stop(true); setIsRunning(false); super.stop(); } } } // quickfix.Application methods public void fromAdmin(Message message, SessionID sessionID) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue,RejectLogon { Header header = message.getHeader(); String msgType = header.getString(MsgType.FIELD); if (MsgType.LOGON.equals(msgType)) { setFeedStatus(FeedStatus.AVAILABLE); SLF4JLoggerProxy.debug(this, "Marketcetera feed received Logon"); //$NON-NLS-1$ } else if (MsgType.LOGOUT.equals(msgType)) { SLF4JLoggerProxy.debug(this, "Marketcetera feed received Logout"); //$NON-NLS-1$ } else if (!MsgType.HEARTBEAT.equals(msgType)) { SLF4JLoggerProxy.debug(this, "Admin message for Marketcetera feed: {}", //$NON-NLS-1$ message); } } public void fromApp(Message message, SessionID sessionID) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { String reqID = null; boolean handled = false; try { StringField correlationField = FIXMessageUtil.getCorrelationField(FIXVersion.FIX44, message.getHeader().getString(MsgType.FIELD)); reqID = message.getString(correlationField.getTag()); } catch (FieldNotFound fnf) { // not every message needs to have reqID, this is OK } if (reqID != null && reqID.length() > 0) { synchronized (pendingRequests) { for (String requestID : pendingRequests.keySet()) { if (requestID.equals(reqID)){ try { // the other side should wait on this before we can call exchange pendingRequests.get(requestID).exchange(message, 1, TimeUnit.NANOSECONDS); handled = true; } catch (Exception e) { // calling side probably timed out... EXCHANGE_ERROR.error(this, e); } break; } } pendingRequests.remove(reqID); } } if (!handled){ fireMarketDataMessage(message); } } public void onCreate(SessionID sessionID) { SLF4JLoggerProxy.debug(this, "Marketcetera feed session created {}", //$NON-NLS-1$ sessionID); } public void onLogon(SessionID sessionID) { setFeedStatus(FeedStatus.AVAILABLE); } public void onLogout(SessionID sessionID) { setFeedStatus(FeedStatus.OFFLINE); } public void toAdmin(Message message, SessionID sessionID) { } public void toApp(Message message, SessionID sessionID) throws DoNotSend { } private void fireMarketDataMessage(Message refresh) { String symbol; try { symbol = refresh.getString(Symbol.FIELD); } catch (FieldNotFound e) { symbol = UNKNOWN_SYMBOL; } Set<String> handles = getHandlesForSymbol(symbol); SLF4JLoggerProxy.debug(this, "MarketceteraFeed received response for handle(s): {}", //$NON-NLS-1$ handles); for(String handle : handles) { dataReceived(handle, refresh); } } private MarketceteraFeed(String inProviderName) throws URISyntaxException, CoreException { super(FeedType.UNKNOWN, inProviderName); try { idFactory = new InMemoryIDFactory(System.currentTimeMillis(), String.format("-%s-", //$NON-NLS-1$ InetAddress.getLocalHost().toString())); } catch (UnknownHostException e) { throw new IllegalArgumentException(e); } } /** * used in a message that does not contain a symbol */ private static final String UNKNOWN_SYMBOL = "unknown"; //$NON-NLS-1$ /** * singleton instance of the marketcetera feed */ private static MarketceteraFeed sInstance; /** * Gets an instance of <code>MarketceteraFeed</code>. * * @param inProviderName a <code>String</code> value * @return a <code>MarketceteraFeed</code> value * @throws CoreException * @throws URISyntaxException */ public static MarketceteraFeed getInstance(String inProviderName) throws URISyntaxException, CoreException { if(sInstance != null) { return sInstance; } sInstance = new MarketceteraFeed(inProviderName); return sInstance; } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#doCancel(java.lang.String) */ @Override protected void doCancel(String inHandle) { SLF4JLoggerProxy.debug(this, "Marketcetera feed canceling subscriptions for handle {}", //$NON-NLS-1$ inHandle); Request request = removeRequest(inHandle); FIXCorrelationFieldSubscription subscription = request.getSubscription(); Message message = messageFactory.create("", //$NON-NLS-1$ subscription.getSubscribeMsgType()); StringField correlationID = FIXMessageUtil.getCorrelationField(FIXVersion.FIX44, subscription.getSubscribeMsgType()); correlationID.setValue(subscription.toString()); SLF4JLoggerProxy.debug(this, "Marketcetera feed sending cancel request for {}", //$NON-NLS-1$ correlationID); message.setField(correlationID); message.setField(new SubscriptionRequestType(SubscriptionRequestType.DISABLE_PREVIOUS_SNAPSHOT_PLUS_UPDATE_REQUEST)); message.setField(new NoRelatedSym(0)); message.setField(new NoMDEntryTypes(0)); if (subscription.getMarketDepth() != null) { message.setField(new MarketDepth(subscription.getMarketDepth())); } try { sendMessage(message); } catch (SessionNotFound e) { throw new IllegalArgumentException(e); } } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#doLogin(org.marketcetera.marketdata.IMarketDataFeedCredentials) */ @Override protected boolean doLogin(MarketceteraFeedCredentials inCredentials) { credentials = inCredentials; try { connectToServer(); } catch (Exception e) { SLF4JLoggerProxy.error(this, e); return false; } return true; } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#doLogout() */ @Override protected void doLogout() { stop(); } /** * Associates the given request handles with the given request object. * * @param inRequest a <code>Request</code> value */ private synchronized static void addRequest(Request inRequest) { requestsByHandle.put(inRequest.getIdAsString(), inRequest); for(String symbol : inRequest.getRequest().getSymbols()) { handlesBySymbol.put(symbol, inRequest.getIdAsString()); } } /** * Returns the handles associated with the given symbol, if any. * * @param inSymbol a <code>String</code> value * @return a <code>Set<String></code> value */ private synchronized static Set<String> getHandlesForSymbol(String inSymbol) { Set<String> handles = handlesBySymbol.get(inSymbol); if(handles != null) { return handles; } return Collections.emptySet(); } /** * Returns the <code>Request</code> associated with the given handle. * * @param inHandle a <code>String</code> value * @return a <code>Request</code> value or null */ synchronized static Request getRequestByHandle(String inHandle) { return requestsByHandle.get(inHandle); } /** * Removes all subscriptions for the given handle. * * @param inHandle a <code>String</code> value * @return a <code>Request</code> value */ private synchronized Request removeRequest(String inHandle) { Request request = requestsByHandle.remove(inHandle); for(String symbol : request.getRequest().getSymbols()) { Set<String> handles = handlesBySymbol.get(symbol); handles.remove(inHandle); } return request; } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#generateToken(org.marketcetera.marketdata.MarketDataFeedTokenSpec) */ @Override protected MarketceteraFeedToken generateToken(MarketDataFeedTokenSpec inTokenSpec) throws FeedException { return MarketceteraFeedToken.getToken(inTokenSpec, this); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#getEventTranslator() */ @Override protected MarketceteraFeedEventTranslator getEventTranslator() { return MarketceteraFeedEventTranslator.getInstance(); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#getMessageTranslator() */ @Override protected MarketceteraFeedMessageTranslator getMessageTranslator() { return MarketceteraFeedMessageTranslator.getInstance(); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#isLoggedIn(org.marketcetera.marketdata.IMarketDataFeedCredentials) */ @Override protected boolean isLoggedIn() { return isRunning(); } /* (non-Javadoc) * @see org.marketcetera.marketdata.AbstractMarketDataFeed#doMarketDataRequest(java.lang.Object) */ @Override protected List<String> doMarketDataRequest(Request inData) throws FeedException { try { inData.setSubscription(doQuery(inData.getMessage())); addRequest(inData); SLF4JLoggerProxy.debug(this, "MarketceteraFeed posted query for {} and associated the request with handle {}", //$NON-NLS-1$ inData.getRequest().getSymbols(), inData.getIdAsString()); return Arrays.asList(new String[] { inData.getIdAsString() } ); } catch (Exception e) { throw new FeedException(e); } } /** * active requests by handle */ private static final Map<String,Request> requestsByHandle = new HashMap<String,Request>(); /** * handles by associated symbol */ private static final SetMultimap<String,String> handlesBySymbol = HashMultimap.create(); /** * Represents a request made to the marketcetera adapter. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: MarketceteraFeed.java 16893 2014-04-25 18:20:56Z colin $ * @since 1.5.0 */ @ClassVersion("$Id: MarketceteraFeed.java 16893 2014-04-25 18:20:56Z colin $") static final class Request { /** * the FIX message actually sent to the marketcetera feed */ private final Message message; /** * the underlying request submitted to the adapter */ private final MarketDataRequest request; /** * the unique identifier for this request */ private final long id; /** * the subscription token returned from the submit call */ private FIXCorrelationFieldSubscription subscription; /** * Create a new Request instance. * * @param inId a <code>long</code> value * @param inMessage a <code>Message</code> value * @param inRequest a <code>MarketDataRequest</code> value */ Request(long inId, Message inMessage, MarketDataRequest inRequest) { id = inId; message = inMessage; request = inRequest; } /** * Get the id value. * * @return a <code>long</code> value */ long getId() { return id; } /** * Gets the id as a <code>String</code>. * * @return a <code>String</code> value */ String getIdAsString() { return Long.toHexString(getId()); } /** * Get the messages value. * * @return a <code>Message</code> value */ Message getMessage() { return message; } /** * Get the request value. * * @return a <code>MarketDataRequest</code> value */ MarketDataRequest getRequest() { return request; } /** * Get the subscription value. * * @return a <code>FIXCorrelationFieldSubscription</code> value or null */ FIXCorrelationFieldSubscription getSubscription() { return subscription; } /** * Sets the subscription value. * * @param a <code>FIXCorrelationFieldSubscription</code> value */ private void setSubscription(FIXCorrelationFieldSubscription inSubscription) { subscription = inSubscription; } } }