package org.marketcetera.messagehistory; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.Callable; import org.marketcetera.core.instruments.UnderlyingSymbolSupport; import org.marketcetera.quickfix.FIXMessageFactory; import org.marketcetera.trade.ExecutionReport; import org.marketcetera.trade.Instrument; import org.marketcetera.trade.OrderID; import org.marketcetera.trade.OrderStatus; import org.marketcetera.trade.Originator; import org.marketcetera.trade.ReportBase; import org.marketcetera.trade.ReportID; import org.marketcetera.util.log.SLF4JLoggerProxy; import org.marketcetera.util.misc.ClassVersion; import quickfix.Message; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.FilterList; import ca.odell.glazedlists.FunctionList; import ca.odell.glazedlists.GlazedLists; import ca.odell.glazedlists.GroupingList; /* $License$ */ /** * Keeps track of Trading Report History for photon. * * @author anshul@marketcetera.com * @author <a href="mailto:will@marketcetera.com">Will Horn</a> * @version $Id: TradeReportsHistory.java 16888 2014-04-22 18:32:36Z colin $ * @since 1.0.0 */ @ClassVersion("$Id: TradeReportsHistory.java 16888 2014-04-22 18:32:36Z colin $") public class TradeReportsHistory { private final EventList<ReportHolder> mAllMessages; private final EventList<ReportHolder> mReadOnlyAllMessages; private final EventList<ReportHolder> mReadOnlyFillMessages; private final AveragePriceReportList mAveragePriceList; private final EventList<ReportHolder> mReadOnlyAveragePriceList; private final FilterList<ReportHolder> mLatestExecutionReportsList; private final FilterList<ReportHolder> mLatestMessageList; private final EventList<ReportHolder> mOpenOrderList; private final EventList<ReportHolder> mReadOnlyOpenOrderList; private final Map<OrderID, ReportHolder> mOriginalOrderACKs; private final Map<OrderID, OrderID> mOrderIDToGroupMap; private final Set<ReportID> mUniqueReportIds = new HashSet<ReportID>(); private final ca.odell.glazedlists.util.concurrent.Lock mReadLock; private final ca.odell.glazedlists.util.concurrent.Lock mWriteLock; /** * Queue of incoming reports than could not be processed immediately due to an in progress reset * operation */ private final Queue<ReportBase> mQueuedReports = new LinkedList<ReportBase>(); /** * Indicates that the queue should be used instead of waiting to add reports directly */ private boolean mQueueMessages = false; private final UnderlyingSymbolSupport mUnderlyingSymbolSupport; /** * Constructor. * * @param messageFactory * factory for creating FIX messages (used for average price * lists) * @param underlyingSymbolSupport * underlying symbol support (allows execution reports to be * annotated with underlying symbol, e.g. for filtering by * underlying) */ public TradeReportsHistory(FIXMessageFactory messageFactory, UnderlyingSymbolSupport underlyingSymbolSupport) { mUnderlyingSymbolSupport = underlyingSymbolSupport; mAllMessages = new BasicEventList<ReportHolder>(); mReadLock = mAllMessages.getReadWriteLock().readLock(); mWriteLock = mAllMessages.getReadWriteLock().writeLock(); mReadOnlyAllMessages = GlazedLists.readOnlyList(mAllMessages); mReadOnlyFillMessages = GlazedLists.readOnlyList(new FilterList<ReportHolder>(mAllMessages, new ReportFillMatcher())); GroupingList<ReportHolder> orderIDList = new GroupingList<ReportHolder>(mAllMessages, new ReportGroupIDComparator()); mLatestExecutionReportsList = new FilterList<ReportHolder>( new FunctionList<List<ReportHolder>, ReportHolder>(orderIDList, new LatestExecutionReportFunction()), new NotNullReportMatcher()); mLatestMessageList = new FilterList<ReportHolder>( new FunctionList<List<ReportHolder>, ReportHolder>(orderIDList, new LatestReportFunction()), new NotNullReportMatcher()); mAveragePriceList = new AveragePriceReportList(messageFactory, mAllMessages); mReadOnlyAveragePriceList = GlazedLists.readOnlyList(mAveragePriceList); mOpenOrderList = new FilterList<ReportHolder>(new FunctionList<List<ReportHolder>,ReportHolder>(orderIDList, new OpenOrderListFunction()), new NotNullReportMatcher()); mReadOnlyOpenOrderList = GlazedLists.readOnlyList(mOpenOrderList); mOriginalOrderACKs = new HashMap<OrderID, ReportHolder>(); mOrderIDToGroupMap = new HashMap<OrderID, OrderID>(); } /** * Resets the history to a new set of reports retrieved using the provided Callable. This method * effectively clears the lists and adds the given reports as if they were added using * {@link #addIncomingMessage(ReportBase)}. * <p> * <strong>All reports added before this method call will be lost.</strong> * * @param reportsRetriever * retrieves the new reports * @throws Exception if reportsRetriever throws an exception */ public void resetMessages(Callable<ReportBase[]> reportsRetriever) throws Exception { // queue new incoming messages synchronized (mQueuedReports) { mQueueMessages = true; } try { // acquire write lock to clear the lists mWriteLock.lock(); try { // clear the list and supporting data structures mAllMessages.clear(); mUniqueReportIds.clear(); mOriginalOrderACKs.clear(); mOrderIDToGroupMap.clear(); } finally { mWriteLock.unlock(); } // retrieve new reports and add them ReportBase[] reports = new ReportBase[0]; reports = reportsRetriever.call(); for (ReportBase report : reports) { // this call gets the write lock internalAddIncomingMessage(report); } } finally { // flush the queue synchronized (mQueuedReports) { try { for (ReportBase report : mQueuedReports) { internalAddIncomingMessage(report); } } finally { mQueueMessages = false; mQueuedReports.clear(); } } } } /** * Adds a new report to the base list. Duplicates are ignored. * * The report might not be added immediately if a reset is in progress. In this case, it will be * queued until the reset has completed. * * @param inReport */ public void addIncomingMessage(ReportBase inReport) { // wait if queue is being emptied synchronized (mQueuedReports) { if (mQueueMessages) { mQueuedReports.add(inReport); } else { internalAddIncomingMessage(inReport); } } } private void internalAddIncomingMessage(ReportBase inReport) { mWriteLock.lock(); try { // check for duplicates ReportID uniqueID = inReport.getReportID(); if(uniqueID == null) { SLF4JLoggerProxy.debug(this, "Recieved report without report id: {}", inReport); //$NON-NLS-1$ } else { if (mUniqueReportIds.contains(uniqueID)) { SLF4JLoggerProxy.debug(this, "Skipping duplicate report: {}", inReport); //$NON-NLS-1$ return; } else { mUniqueReportIds.add(uniqueID); } } if(SLF4JLoggerProxy.isDebugEnabled(this) && inReport.getSendingTime() != null) { long sendingTime =0; sendingTime = inReport.getSendingTime().getTime(); long systemTime = System.currentTimeMillis(); double diff = (sendingTime-systemTime)/1000.0; if(Math.abs(diff) > 1) { SLF4JLoggerProxy.debug(this, "{}: sendingTime v systemTime: {}", //$NON-NLS-1$ Thread.currentThread().getName(), diff); } } updateOrderIDMappings(inReport); OrderID groupID = getGroupID(inReport); String underlying = null; if(inReport instanceof ExecutionReport) { Instrument instrument = ((ExecutionReport) inReport).getInstrument(); underlying = mUnderlyingSymbolSupport.getUnderlying(instrument); } ReportHolder messageHolder = new ReportHolder(inReport, underlying, groupID); // The first message that comes in with a specific order id gets stored in a map. This // map is used by #getFirstReport(String) to facilitate CancelReplace if (inReport instanceof ExecutionReport && inReport.getOrderID() != null) { OrderID id = inReport.getOrderID(); OrderStatus status = inReport.getOrderStatus(); if (Originator.Server == ((ExecutionReport) inReport) .getOriginator() && (status == OrderStatus.PendingNew || status == OrderStatus.PendingReplace)) { if (!mOriginalOrderACKs.containsKey(id)) { mOriginalOrderACKs.put(id, messageHolder); } } } mAllMessages.add(messageHolder); } finally { mWriteLock.unlock(); } } private void updateOrderIDMappings(ReportBase inReport) { if (inReport.getOrderID() != null && inReport.getOriginalOrderID() != null) { OrderID origOrderID = inReport.getOriginalOrderID(); OrderID orderID = inReport.getOrderID(); OrderID groupID; // first check to see if the orig is in the map, and if so, use // whatever it maps to as the groupID if (mOrderIDToGroupMap.containsKey(origOrderID)){ groupID = getGroupID(origOrderID); } else { // otherwise, do a mapping from clOrdId -> origOrderID groupID = origOrderID; } mOrderIDToGroupMap.put(orderID, groupID); } } private OrderID getGroupID(ReportBase inReport) { return getGroupID(inReport.getOrderID()); } private OrderID getGroupID(OrderID clOrdID) { if (mOrderIDToGroupMap.containsKey(clOrdID)){ return mOrderIDToGroupMap.get(clOrdID); } else { return clOrdID; } } public EventList<ReportHolder> getFillsList() { return mReadOnlyFillMessages; } public EventList<ReportHolder> getAveragePricesList() { return mReadOnlyAveragePriceList; } public int size() { mReadLock.lock(); try { return mAllMessages.size(); } finally { mReadLock.unlock(); } } public ExecutionReport getLatestExecutionReport(OrderID clOrdID) { return (ExecutionReport) getReport(mLatestExecutionReportsList, clOrdID); } private ReportBase getReport(EventList<ReportHolder> list, OrderID clOrdID) { mReadLock.lock(); try { OrderID groupID = getGroupID(clOrdID); if (groupID != null){ for (ReportHolder holder : list) { if (groupID.equals(holder.getGroupID())){ return holder.getReport(); } } } return null; } finally { mReadLock.unlock(); } } public EventList<ReportHolder> getAllMessagesList() { return mReadOnlyAllMessages; } public Message getLatestMessage(OrderID inOrderID) { mReadLock.lock(); try { OrderID groupID = getGroupID(inOrderID); if (groupID != null) { for (ReportHolder holder : mLatestMessageList) { OrderID holderGroupID = holder.getGroupID(); if (holderGroupID != null && groupID.equals(holderGroupID)){ return holder.getMessage(); } } } return null; } finally { mReadLock.unlock(); } } public EventList<ReportHolder> getOpenOrdersList() { return mReadOnlyOpenOrderList; } public void visitOpenOrdersExecutionReports(MessageVisitor visitor) { mReadLock.lock(); try { ReportHolder[] holders = mOpenOrderList.toArray(new ReportHolder[mOpenOrderList.size()]); for(ReportHolder holder : holders) { visitor.visitOpenOrderExecutionReports(holder.getReport()); } } finally { mReadLock.unlock(); } } /** * Returns a {@link org.marketcetera.messagehistory.ReportHolder} holding the first report Photon received * for the given clOrdID. This is the PENDING NEW or PENDING REPLACE message * added via {@link #addIncomingMessage(ReportBase)}. * * @param inOrderID the orderID. * @return the ReportHolder holding the first report */ public ReportHolder getFirstReport(OrderID inOrderID){ mReadLock.lock(); try { return mOriginalOrderACKs.get(inOrderID); } finally { mReadLock.unlock(); } } }