package org.marketcetera.trade.utils; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.ThreadSafe; import org.marketcetera.marketdata.DateUtils; import org.marketcetera.trade.ExecutionReport; import org.marketcetera.trade.Messages; import org.marketcetera.trade.OrderID; import org.marketcetera.trade.ReportBase; import org.marketcetera.util.collections.UnmodifiableDeque; import org.marketcetera.util.log.SLF4JLoggerProxy; import org.marketcetera.util.misc.ClassVersion; import org.nocrala.tools.texttablefmt.*; import org.nocrala.tools.texttablefmt.CellStyle.HorizontalAlign; /* $License$ */ /** * Manages order history for multiple orders throughout the order lifecycle. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: OrderHistoryManager.java 16892 2014-04-24 20:58:37Z colin $ * @since 2.1.4 */ @ThreadSafe @ClassVersion("$Id: OrderHistoryManager.java 16892 2014-04-24 20:58:37Z colin $") public class OrderHistoryManager { /** * Gets the root order ID for the given order ID. * * @param inOrderID an <code>OrderID</code> value * @return an <code>OrderID</code> value or <code>null</code> */ public OrderID getRootOrderIdFor(OrderID inOrderID) { Deque<ReportBase> orders = getReportHistoryFor(inOrderID); if(orders == null || orders.isEmpty()) { return null; } return orders.getLast().getOrderID(); } /** * Gets the latest <code>ReportBase</code> for the given <code>OrderID</code>. * * <p>The given <code>OrderID</code> may correspond to either the * actual order ID or order ID of a replaced order in the same order chain. * * <p>The returned <code>ReportBase</code> is the most recent report at a static point in time only. * Changes to the underlying order history are not reflected in the returned <code>ReportBase</code>. * * @param inOrderID an <code>OrderID</code> value * @return a <code>ReportBase</code> or <code>null</code> if there is no known status for the given <code>OrderID</code> */ public ReportBase getLatestReportFor(OrderID inOrderID) { SLF4JLoggerProxy.debug(this, "Searching order tracker for {}", //$NON-NLS-1$ inOrderID); synchronized(orders) { OrderHistory history = orders.get(inOrderID); if(history != null) { ReportBase report = history.getLatestReport(); SLF4JLoggerProxy.debug(this, "Retrieved {} for {}", report, inOrderID); return report; } SLF4JLoggerProxy.debug(this, "No history for {}", //$NON-NLS-1$ inOrderID); return null; } } /** * Adds the given <code>ReportBase</code> to the order history. * * @param inReport a <code>ReportBase</code> value */ public void add(ReportBase inReport) { if(inReport.getOrderStatus() == null || inReport.getOrderID() == null) { Messages.SKIPPNG_MALFORMED_REPORT.warn(this, inReport); return; } SLF4JLoggerProxy.debug(this, "Adding {} to order history", inReport); synchronized(orders) { OrderID actualOrderID = inReport.getOrderID(); OrderID originalOrderID = inReport.getOriginalOrderID(); // find the order history for this report // first, look for a match of the actual order ID (simple, non-replace order case) OrderHistory history = orders.get(actualOrderID); if(history == null) { // ok, no order history for the actual order ID. this is caused by one of two things: // 1/ This is the first time we've seen anything in this chain // 2/ The report is a replace order and we should search using the originalOrderID history = orders.get(originalOrderID); if(history == null) { // now we know this is case #1 from above: create a new order history and add it history = new OrderHistory(); // index the new history using the actual order ID orders.put(actualOrderID, history); SLF4JLoggerProxy.debug(this, "Created new {} for actual order ID: {} because there was no order history for this actual order ID nor the original order ID: {}", history, actualOrderID, originalOrderID); } else { // case #2 from above: add an index reference for the new actual order ID orders.put(actualOrderID, history); SLF4JLoggerProxy.debug(this, "Using existing {} for actual order ID: {} because there was already history for original order ID: {}", history, actualOrderID, originalOrderID); } } else { SLF4JLoggerProxy.debug(this, "Selected order history {} based on actual orderID: {} from {}", history, actualOrderID, orders); } // add the report to the order history history.add(inReport); SLF4JLoggerProxy.debug(this, "Added {} to {}", inReport, history); // check to see if the report represents an open order if(inReport.getOrderStatus().isCancellable()) { // if a report is cancellable, at least by our current understanding, the report has to be an ExecutionReport (not an OrderCancelReject) if(inReport instanceof ExecutionReport) { SLF4JLoggerProxy.debug(this, "{} represents an open order ({}), updating live order list for {}", //$NON-NLS-1$ inReport.getOrderID(), inReport.getOrderStatus(), history); openOrders.put(inReport.getOrderID(), (ExecutionReport)inReport); } } else { SLF4JLoggerProxy.debug(this, "{} represents a closed order ({}) updating live order list for {}", //$NON-NLS-1$ inReport.getOrderID(), inReport.getOrderStatus(), history); openOrders.remove(inReport.getOrderID()); } if(inReport.getOriginalOrderID() != null) { SLF4JLoggerProxy.debug(this, "{} replaces {}, updating live order list", //$NON-NLS-1$ inReport.getOrderID(), inReport.getOriginalOrderID()); openOrders.remove(inReport.getOriginalOrderID()); } if(SLF4JLoggerProxy.isTraceEnabled(this)) { SLF4JLoggerProxy.trace(this, display()); } } synchronized(this) { this.notifyAll(); } } /** * Displays the current order history in a readable format. * * <p>This operation may be very expensive. * * @return a <code>String</code> value */ public String display() { synchronized(orders) { StringBuffer output = new StringBuffer(); output.append(nl).append("Order History as of ").append(new Date()).append(nl); //$NON-NLS-1$ Table latestReportTable = new Table(10, BorderStyle.CLASSIC_COMPATIBLE_WIDE, ShownBorders.ALL, false); latestReportTable.addCell("OrderID", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Status", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("SendingTime", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("OrderChain", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Side", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Quantity", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Symbol", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Type", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Price", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Text", //$NON-NLS-1$ headerStyle, 1); Set<OrderID> handledOrders = new HashSet<OrderID>(); for(OrderHistory order : orders.values()) { ReportBase report = order.getLatestReport(); if(report != null && !handledOrders.contains(report.getOrderID())) { latestReportTable.addCell(order.getLatestReport().getOrderID().getValue()); latestReportTable.addCell(order.getLatestReport().getOrderStatus().name()); latestReportTable.addCell(DateUtils.dateToString(order.getLatestReport().getSendingTime())); latestReportTable.addCell(order.getOrderIdChain().toString()); latestReportTable.addCell(report instanceof ExecutionReport ? ((ExecutionReport)report).getSide().name() : none); latestReportTable.addCell(report instanceof ExecutionReport ? String.valueOf(((ExecutionReport)report).getOrderQuantity()) : none); latestReportTable.addCell(report instanceof ExecutionReport ? ((ExecutionReport)report).getInstrument().getSymbol() : none); latestReportTable.addCell(report instanceof ExecutionReport ? String.valueOf(((ExecutionReport)report).getOrderType()) : none); latestReportTable.addCell(report instanceof ExecutionReport ? String.valueOf(((ExecutionReport)report).getPrice()) : none); latestReportTable.addCell(order.getLatestReport().getText()); handledOrders.add(report.getOrderID()); } } output.append(nl); for(String line : latestReportTable.renderAsStringArray()) { output.append(line).append(nl); } output.append(nl).append("Open Orders").append(nl); //$NON-NLS-1$ latestReportTable = new Table(9, BorderStyle.CLASSIC_COMPATIBLE_WIDE, ShownBorders.ALL, false); latestReportTable.addCell("OrderID", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Status", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("SendingTime", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Side", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Quantity", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Symbol", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Type", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Price", //$NON-NLS-1$ headerStyle, 1); latestReportTable.addCell("Text", //$NON-NLS-1$ headerStyle, 1); for(ReportBase report : openOrders.values()) { latestReportTable.addCell(report.getOrderID().getValue()); latestReportTable.addCell(report.getOrderStatus().name()); latestReportTable.addCell(DateUtils.dateToString(report.getSendingTime())); latestReportTable.addCell(report instanceof ExecutionReport ? ((ExecutionReport)report).getSide().name() : none); latestReportTable.addCell(report instanceof ExecutionReport ? String.valueOf(((ExecutionReport)report).getOrderQuantity()) : none); latestReportTable.addCell(report instanceof ExecutionReport ? ((ExecutionReport)report).getInstrument().getSymbol() : none); latestReportTable.addCell(report instanceof ExecutionReport ? String.valueOf(((ExecutionReport)report).getOrderType()) : none); latestReportTable.addCell(report instanceof ExecutionReport ? String.valueOf(((ExecutionReport)report).getPrice()) : none); latestReportTable.addCell(report.getText()); } output.append(nl); for(String line : latestReportTable.renderAsStringArray()) { output.append(line).append(nl); } return output.toString(); } } /** * Gets the <code>ReportBase</code> values for the given <code>OrderID</code>. * * <p>The <code>ReportBase</code> collection returned is sorted from newest to oldest. * * <p>The returned <code>Deque</code> does not change and will not reflect future changes. * * <p>The given <code>OrderID</code> may be either an order ID or an original order ID. The reports * returned will be the same in either case. If no history exists for the given <code>OrderID<code>, * an empty <code>Deque</code> is returned. * * <p>The underlying order history is populated by calls to {@link #add(ReportBase)}. * * @param inOrderId an <code>OrderID</code> value * @return a <code>Deque<ReportBase></code> value which may be empty */ public Deque<ReportBase> getReportHistoryFor(OrderID inOrderId) { if(inOrderId == null) { throw new NullPointerException(); } synchronized(orders) { OrderHistory history = orders.get(inOrderId); if(history == null) { return NO_ORDER_HISTORY; } return history.getOrderHistory(); } } /** * Gets the open orders. * * <p>The collection returned by this operation will reflect changes to the underlying order history. * * @return a <code>Map<OrderID,ExecutionReport></code> value */ public Map<OrderID,ExecutionReport> getOpenOrders() { return Collections.unmodifiableMap(openOrders); } /** * Gets all <code>OrderID</code> values for which history is known. * * <p>The returned collection will be updated as new order history is received. The * sort order of the returned collection is unspecified. * * @return a <code>Set<OrderID></code> value */ public Set<OrderID> getOrderIds() { synchronized(orders) { return Collections.unmodifiableSet(orders.keySet()); } } /** * Clears all order history. */ public void clear() { synchronized(orders) { for(OrderHistory history : orders.values()) { history.clear(); } orders.clear(); openOrders.clear(); } } /** * Clears the order history values for the given <code>OrderID</code> if any. * * <p>This method has no effect if there is no stored order history for the * given <code>OrderID</code>. * * <p>This method will clear the order history for all orders in the order history * chain including replaced orders. * * @param inOrderId an <code>OrderID</code> value */ public void clear(OrderID inOrderId) { synchronized(orders) { OrderHistory history = orders.get(inOrderId); if(history != null) { for(OrderID orderID : history.getOrderIdChain()) { SLF4JLoggerProxy.debug(this, "Clearing history for {}", //$NON-NLS-1$ orderID); orders.remove(orderID); } history.clear(); } if(inOrderId != null) { openOrders.remove(inOrderId); } } } /** * Gets the chain of <code>OrderID</code> values that describe the evolution of the * given order. * * <p>Changes to the underlying order history will be reflected in the * returned collection. * * <p>OrderIDs are sorted from oldest to newest. * * <p>If the given <code>OrderID</code> has no corresponding order history, the * returned collection will be empty. * * @return a <code>Set<OrderID></code> value */ public Set<OrderID> getOrderChain(OrderID inOrderId) { synchronized(orders) { OrderHistory history = orders.get(inOrderId); if(history != null) { return history.getOrderIdChain(); } return NO_ORDER_CHAIN; } } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("OrderHistoryManager with history for: ").append(orders.keySet()); //$NON-NLS-1$ return builder.toString(); } /** * Tracks order history for a single order. * * <p>Instantiate this object and add <code>ReportBase</code> objects for * this same order chain. No validation is done to make sure that incoming * <code>ReportBase</code> objects are truly part of the order chain: the * act of invoking <code>add</code> implicitly establishes this fact. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: OrderHistoryManager.java 16892 2014-04-24 20:58:37Z colin $ * @since 2.1.4 */ @NotThreadSafe @ClassVersion("$Id: OrderHistoryManager.java 16892 2014-04-24 20:58:37Z colin $") private static class OrderHistory { /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("OrderHistory [").append(latestReport == null ? "none" : latestReport.getOrderID()).append("]"); return builder.toString(); } /** * Adds the given <code>ReportBase</code> to the order history. * * @param inReport a <code>ReportBase</code> value */ private void add(ReportBase inReport) { orderHistory.addFirst(inReport); orderIdChain.add(inReport.getOrderID()); latestReport = inReport; } /** * Clears the order history object. */ private void clear() { orderHistory.clear(); orderIdChain.clear(); latestReport = null; } /** * Gets the order history. * * <p>The reports returned are sorted from newest to oldest. * * <p>Changes to the underlying order history will be reflected in the * returned collection. * * @return a <code>Deque<ReportBase></code> value */ private Deque<ReportBase> getOrderHistory() { return new UnmodifiableDeque<ReportBase>(orderHistory); } /** * Get the latestReport value. * * @return a <code>ReportBase</code> value */ private ReportBase getLatestReport() { return latestReport; } /** * Gets the chain of <code>OrderID</code> values that describe the evolution of this * order. * * <p>Changes to the underlying order history will be reflected in the * returned collection. * * <p>OrderIDs are sorted from oldest to newest. * * @return a <code>Set<OrderID></code> value */ private Set<OrderID> getOrderIdChain() { return Collections.unmodifiableSet(orderIdChain); } /** * order history sorted from newest to oldest */ private final Deque<ReportBase> orderHistory = new LinkedList<ReportBase>(); /** * order IDs in the order chain in the order they occurred */ private final Set<OrderID> orderIdChain = new LinkedHashSet<OrderID>(); /** * most recent <code>ExecutionReport</code>, may be <code>null</code> */ private volatile ReportBase latestReport; } /** * order history objects indexed by actual order ID */ @GuardedBy("orders") private final Map<OrderID,OrderHistory> orders = new LinkedHashMap<OrderID,OrderHistory>(); /** * collection containing only the open orders */ @GuardedBy("orders") private final Map<OrderID,ExecutionReport> openOrders = new ConcurrentHashMap<OrderID,ExecutionReport>(); /** * sentinel collection used to indicate there is no order chain for a given order ID */ private static final Set<OrderID> NO_ORDER_CHAIN = Collections.emptySet(); /** * sentinel collection used to indicate there is no order history for a given order ID */ private static final Deque<ReportBase> NO_ORDER_HISTORY = new UnmodifiableDeque<ReportBase>(new LinkedList<ReportBase>()); /** * constant used to separate lines in status display */ private static final String nl = System.getProperty("line.separator"); //$NON-NLS-1$ /** * the style used for display tables */ private static final CellStyle headerStyle = new CellStyle(HorizontalAlign.center); /** * display constant used to represent the lack of data */ private static final String none = "---"; //$NON-NLS-1$ }