package org.marketcetera.messagehistory; import static java.math.BigDecimal.ZERO; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.HashMap; import org.marketcetera.core.instruments.InstrumentFromMessage; import org.marketcetera.core.instruments.InstrumentToMessage; import org.marketcetera.quickfix.FIXMessageFactory; import org.marketcetera.trade.*; import org.marketcetera.util.log.SLF4JLoggerProxy; import org.marketcetera.util.misc.ClassVersion; import quickfix.FieldNotFound; import quickfix.Message; import quickfix.field.AvgPx; import quickfix.field.CumQty; import quickfix.field.MsgType; import ca.odell.glazedlists.AbstractEventList; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.event.ListEvent; import ca.odell.glazedlists.event.ListEventListener; /* $License$ */ /** * A virtual list of {@link ReportHolder} that tracks the average price of * symbols in a source list. This list will have one entry for each unique * symbol in the source list. * * @author anshul@marketcetera.com * @author <a href="mailto:will@marketcetera.com">Will Horn</a> * @version $Id: AveragePriceReportList.java 16888 2014-04-22 18:32:36Z colin $ * @since 1.0.0 */ @ClassVersion("$Id: AveragePriceReportList.java 16888 2014-04-22 18:32:36Z colin $") public class AveragePriceReportList extends AbstractEventList<ReportHolder> implements ListEventListener<ReportHolder> { private final HashMap<SymbolSide, Integer> mAveragePriceIndexes = new HashMap<SymbolSide, Integer>(); private final ArrayList<ReportHolder> mAveragePricesList = new ArrayList<ReportHolder>(); private final FIXMessageFactory mMessageFactory; public AveragePriceReportList(FIXMessageFactory messageFactory, EventList<ReportHolder> source) { super(source.getPublisher()); this.mMessageFactory = messageFactory; source.addListEventListener(this); readWriteLock = source.getReadWriteLock(); } public void listChanged(ListEvent<ReportHolder> listChanges) { // all of these changes to this list happen "atomically" updates.beginEvent(true); // handle reordering events if(!listChanges.isReordering()) { // for all changes, one index at a time while(listChanges.next()) { // get the current change info int changeType = listChanges.getType(); EventList<ReportHolder> sourceList = listChanges.getSourceList(); // handle delete events if(changeType == ListEvent.UPDATE) { throw new UnsupportedOperationException(); } else if (changeType == ListEvent.DELETE) { // assume a delete all since this is the only thing supported. clear(); updates.commitEvent(); return; } else if(changeType == ListEvent.INSERT) { ReportHolder deltaReportHolder = sourceList.get(listChanges.getIndex()); Message deltaMessage = deltaReportHolder.getMessage(); ReportBase deltaReport = deltaReportHolder.getReport(); quickfix.field.Side orderSide = new quickfix.field.Side(); try { deltaMessage.getField(orderSide); } catch (FieldNotFound e) { orderSide.setValue(quickfix.field.Side.UNDISCLOSED); } String side = String.valueOf(orderSide.getValue()); Instrument instrument = InstrumentFromMessage.SELECTOR.forValue(deltaMessage).extract(deltaMessage); SymbolSide symbolSide = new SymbolSide(instrument, side); if(deltaReport instanceof ExecutionReport) { SLF4JLoggerProxy.debug(AveragePriceReportList.class, "Considering {}", //$NON-NLS-1$ deltaReport); ExecutionReport execReport = (ExecutionReport)deltaReport; ExecutionType execType = execReport.getExecutionType(); if(execType == null) { SLF4JLoggerProxy.debug(AveragePriceReportList.class, "Skipping {} because the execType was null", //$NON-NLS-1$ execReport); continue; } if(execType == null || !execType.isFill()){ SLF4JLoggerProxy.debug(AveragePriceReportList.class, "Skipping {} because its execution type {} is not a fill", //$NON-NLS-1$ execReport, execReport.getExecutionType()); continue; } if(!execReport.getOriginator().forOrders() || !execReport.getHierarchy().forOrders()) { SLF4JLoggerProxy.debug(AveragePriceReportList.class, "Skipping {} because it's not appropriate for FIX Message Views", //$NON-NLS-1$ execReport); continue; } BigDecimal lastQuantity = execReport.getLastQuantity(); BigDecimal lastPrice = execReport.getLastPrice(); if(lastQuantity == null || !(lastQuantity.compareTo(BigDecimal.ZERO) > 0)) { SLF4JLoggerProxy.debug(AveragePriceReportList.class, "Skipping {} because the last quantity was null/zero", //$NON-NLS-1$ execReport); continue; } if(lastPrice == null) { SLF4JLoggerProxy.debug(AveragePriceReportList.class, "Skipping {} because the last price was null", //$NON-NLS-1$ execReport); continue; } Integer averagePriceIndex = mAveragePriceIndexes.get(symbolSide); // decide if we've seen this symbol/side combination in the list of ERs before. if we have, averagePriceIndex will be non-null if(averagePriceIndex != null) { // we have already processed at least one ER with this symbol/side combination. that means the math must take into account the existing // ERs as well as the current ER ReportHolder averagePriceReportHolder = mAveragePricesList.get(averagePriceIndex); Message averagePriceMessage = averagePriceReportHolder.getMessage(); ExecutionReport averagePriceReport = (ExecutionReport) averagePriceReportHolder.getReport(); BigDecimal existingCumQty = averagePriceReport.getCumulativeQuantity(); BigDecimal existingAvgPx = averagePriceReport.getAveragePrice(); BigDecimal newLastQty = lastQuantity; BigDecimal newTotal = existingCumQty.add(newLastQty); if(!newTotal.equals(ZERO)) { BigDecimal numerator = existingCumQty.multiply(existingAvgPx).add(newLastQty.multiply(lastPrice)); BigDecimal newAvgPx = numerator.divide(newTotal, 4, RoundingMode.HALF_UP); averagePriceMessage.setDecimal(AvgPx.FIELD, newAvgPx); averagePriceMessage.setDecimal(CumQty.FIELD, newTotal); updates.elementUpdated(averagePriceIndex, averagePriceReportHolder, averagePriceReportHolder); } } else { // we have not seen an ER with this instrument/side combination, make a new average price entry Message averagePriceMessage = mMessageFactory.createMessage(MsgType.EXECUTION_REPORT); averagePriceMessage.setField(orderSide); InstrumentToMessage.SELECTOR.forInstrument(instrument).set(instrument, mMessageFactory.getBeginString(), averagePriceMessage); averagePriceMessage.setField(new CumQty(lastQuantity)); averagePriceMessage.setField(new AvgPx(lastPrice.setScale(4, RoundingMode.HALF_UP))); try { ReportHolder newReport = new ReportHolder(Factory.getInstance().createExecutionReport(averagePriceMessage, execReport.getBrokerID(), Originator.Broker, execReport.getActorID(), execReport.getViewerID()), deltaReportHolder.getUnderlying()); mAveragePricesList.add(newReport); averagePriceIndex = mAveragePricesList.size()-1; mAveragePriceIndexes.put(symbolSide, averagePriceIndex); updates.elementInserted(averagePriceIndex, newReport); } catch (MessageCreationException e) { Messages.UNEXPECTED_ERROR.error(this,e); } } } else { SLF4JLoggerProxy.debug(AveragePriceReportList.class, "Skipping {} because it's not an ExecutionReport", //$NON-NLS-1$ deltaReport); } } } } // commit the changes and notify listeners updates.commitEvent(); } @Override public ReportHolder get(int index) { return mAveragePricesList.get(index); } @Override public int size() { return mAveragePricesList.size(); } @Override public void clear() { // don't do a clear on an empty set if(isEmpty()) return; // create the change event updates.beginEvent(); for(int i = 0, size = size(); i < size; i++) { updates.elementDeleted(0, get(i)); } // do the actual clear mAveragePricesList.clear(); mAveragePriceIndexes.clear(); // fire the event updates.commitEvent(); } @Override public void dispose() { } }