package org.marketcetera.marketdata;
import java.util.*;
import org.apache.commons.lang.SystemUtils;
import org.marketcetera.event.*;
import org.marketcetera.event.impl.DepthOfBookEventBuilder;
import org.marketcetera.event.impl.TopOfBookEventBuilder;
import org.marketcetera.event.util.BookPriceComparator;
import org.marketcetera.trade.Instrument;
import org.marketcetera.util.log.SLF4JLoggerProxy;
import org.marketcetera.util.misc.ClassVersion;
import com.google.common.collect.Lists;
/* $License$ */
/**
* Represents the order book for a given symbol.
*
* <p>An <code>OrderBook</code> is a snapshot of open orders for a given
* security. The orders in an <code>OrderBook</code> may be from a single
* or multiple exchanges. <code>OrderBook</code> imposes no restrictions
* on the source exchange allowing the object to represent the orders from
* a specific exchange or an aggregation of orders from multiple exchanges.
*
* <p>To populate the <code>OrderBook</code>, add {@link QuoteEvent} objects
* via {@link #process(QuoteEvent)}. It is important that the events so added
* all be unique according to the event's natural ordering.
*
* <p>{@link QuoteEvent} objects all have an <code>Action</code> attribute.
* The <code>Action</code> attribute dictates whether the event is inserted
* into the book, or changes or deletes an existing order.
*
* @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a>
* @version $Id: OrderBook.java 16852 2014-03-05 17:07:47Z colin $
* @since 0.6.0
*/
@ClassVersion("$Id: OrderBook.java 16852 2014-03-05 17:07:47Z colin $")
public class OrderBook
implements Messages
{
/**
* indicates the order book has no maximum depth
*/
public final static int UNLIMITED_DEPTH = -1;
/**
* the order book depth if none is specified
*/
public final static int DEFAULT_DEPTH = 10;
/**
* Create a new OrderBook instance a reasonable maximum depth.
*
* <p>The resulting <code>OrderBook</code> will have an
* unlimited maximum depth.
*
* @param inInstrument an <code>Instrument</code> instance
*/
public OrderBook(Instrument inInstrument)
{
this(inInstrument,
UNLIMITED_DEPTH);
}
/**
* Checks the given depth to see if it is a valid maximum book depth.
*
* @param inMaximumDepth an <code>int</code> value
*/
public static void validateMaximumBookDepth(int inMaximumDepth)
{
if(inMaximumDepth != UNLIMITED_DEPTH &&
inMaximumDepth <= 0) {
throw new IllegalArgumentException(ORDER_BOOK_DEPTH_MUST_BE_POSITIVE.getText());
}
}
/**
* Get the instrument value.
*
* @return an <code>Instrument</code> value
*/
public final Instrument getInstrument()
{
return mInstrument;
}
/**
* Get the maxDepth value.
*
* @return a <code>OrderBook</code> value
*/
public int getMaxDepth()
{
return mMaxDepth;
}
/**
* Gets the {@link TopOfBookEvent} view of the order book.
*
* @return a <code>TopOfBook</code> value
*/
public final TopOfBookEvent getTopOfBook()
{
List<BidEvent> bidBook = getBidBook();
List<AskEvent> askBook = getAskBook();
return TopOfBookEventBuilder.topOfBookEvent().withBid(bidBook.isEmpty() ? null : bidBook.get(0))
.withAsk(askBook.isEmpty() ? null : askBook.get(0))
.withInstrument(getInstrument())
.withTimestamp(new Date()).create();
}
/**
* Returns the {@link DepthOfBookEvent} view of the order book.
*
* @return a <code>DepthOfBook</code> value
*/
public final DepthOfBookEvent getDepthOfBook()
{
return DepthOfBookEventBuilder.depthOfBook().withBids(getBidBook())
.withAsks(getAskBook())
.withInstrument(getInstrument()).create();
}
/**
* Gets the current state of the <code>Bid</code> book.
*
* @return a <code>List<BidEvent></code> value
*/
public final List<BidEvent> getBidBook()
{
return mBidBook.getSortedView(BookPriceComparator.bidComparator);
}
/**
* Gets the current state of the <code>Ask</code> book.
*
* @return a <code>List<AskEvent></code> value
*/
public final List<AskEvent> getAskBook()
{
return mAskBook.getSortedView(BookPriceComparator.askComparator);
}
/**
* Processes all the events in the given list.
*
* @param inEvents a <code>List<Event></code> value
* @return a <code>List<QuoteEvent></code> value containing the events displaced by the change, may be empty
* @throws IllegalArgumentException if any quote in the give list is not a <code>QuoteEvent</code> or the event's symbol does not match the book's symbol
*/
public final List<QuoteEvent> processAll(List<Event> inEvents)
{
List<QuoteEvent> results = Lists.newArrayList();
for(Event quote : inEvents) {
if(quote instanceof QuoteEvent) {
results.add(process((QuoteEvent)quote));
} else {
throw new IllegalArgumentException();
}
}
return results;
}
/**
* Processes the given event for the order book.
*
* @param inEvent a <code>QuoteEvent</code> value
* @return a <code>QuoteEvent</code> value containing the event displaced by the change or null
* @throws IllegalArgumentException if the event's symbol does not match the book's symbol
*/
public final QuoteEvent process(QuoteEvent inEvent)
{
// make sure the event is valid before proceeding
checkEvent(inEvent);
SLF4JLoggerProxy.debug(this,
"Received {}\nBook starts at\n{}", //$NON-NLS-1$
inEvent,
this);
QuoteEvent eventToReturn = null;
switch(inEvent.getAction()) {
case ADD :
eventToReturn = addEvent(inEvent);
break;
case DELETE :
removeEvent(inEvent);
break;
case CHANGE :
changeEvent(inEvent);
break;
default:
throw new UnsupportedOperationException();
}
SLF4JLoggerProxy.debug(this,
"Book is now\n{}", //$NON-NLS-1$
this);
return eventToReturn;
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((mInstrument == null) ? 0 : mInstrument.hashCode());
return result;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final OrderBook other = (OrderBook) obj;
if (mInstrument == null) {
if (other.mInstrument != null)
return false;
} else if (!mInstrument.equals(other.mInstrument))
return false;
return true;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
StringBuilder book = new StringBuilder();
book.append(getInstrument()).append(SystemUtils.LINE_SEPARATOR);
book.append(printBook(getBidBook().iterator(),
getAskBook().iterator(),
false));
return book.toString();
}
/**
* Creates a human-readable representation of an order book as defined
* by the given {@link Iterator} values.
*
* @param bidIterator an <code>Iterator<BidEvent></code> value representing the bids to display
* @param askIterator an <code>Iterator<AskEvent></code> value representing the asks to display
* @param inShowExchange a <code>boolean</code> value indicating whether to display the exchange associated with each bid and ask
* @return a <code>String</code> containing the human-readable representation of the order book implied by the given <code>Iterator</code> objects
*/
public static <I extends Instrument> String printBook(Iterator<BidEvent> bidIterator,
Iterator<AskEvent> askIterator,
boolean inShowExchange)
{
List<String> bids = new ArrayList<String>();
List<String> asks = new ArrayList<String>();
int maxSize = 10;
while(true) {
if(bidIterator.hasNext()) {
BidEvent bid = bidIterator.next();
StringBuilder entry = new StringBuilder();
if(inShowExchange) {
entry.append(" ").append(bid.getExchange()); //$NON-NLS-1$
}
entry.append(" ").append(bid.getSize()).append(" ").append(bid.getPrice()).append(" "); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
String line = entry.toString();
maxSize = Math.max(maxSize,
line.length());
bids.add(line);
}
if(askIterator.hasNext()) {
AskEvent ask = askIterator.next();
StringBuilder entry = new StringBuilder();
entry.append(ask.getPrice()).append(" ").append(ask.getSize()); //$NON-NLS-1$
if(inShowExchange) {
entry.append(" ").append(ask.getExchange()); //$NON-NLS-1$
}
maxSize = Math.max(maxSize,
entry.toString().length());
String line = entry.toString();
maxSize = Math.max(maxSize,
line.length());
asks.add(line);
}
if(bidIterator.hasNext() ||
askIterator.hasNext()) {
continue;
} else {
break;
}
}
String bidHeader = "bid"; //$NON-NLS-1$
String askHeader = "ask"; //$NON-NLS-1$
StringBuilder finalBook = new StringBuilder();
finalBook.append("Order Book\n"); //$NON-NLS-1$
finalBook.append(bidHeader);
for(int i=0;i<maxSize-bidHeader.length();i++) {
finalBook.append(" "); //$NON-NLS-1$
}
finalBook.append("| ").append(askHeader).append("\n"); //$NON-NLS-1$ //$NON-NLS-2$
finalBook.append(pad("", //$NON-NLS-1$
maxSize,
'-'));
finalBook.append("+"); //$NON-NLS-1$
finalBook.append(pad("", //$NON-NLS-1$
maxSize,
'-'));
finalBook.append("\n"); //$NON-NLS-1$
Iterator<String> bidStringIterator = bids.iterator();
Iterator<String> askStringIterator = asks.iterator();
while(true) {
if(bidStringIterator.hasNext()) {
String bidString = bidStringIterator.next();
finalBook.append(pad(bidString,
maxSize,
' '));
} else {
finalBook.append(pad("", //$NON-NLS-1$
maxSize,
' '));
}
finalBook.append("|"); //$NON-NLS-1$
if(askStringIterator.hasNext()) {
String askString = askStringIterator.next();
finalBook.append(" ").append(askString); //$NON-NLS-1$
}
finalBook.append("\n"); //$NON-NLS-1$
if(bidStringIterator.hasNext() ||
askStringIterator.hasNext()) {
continue;
} else {
break;
}
}
return finalBook.toString();
}
/**
* Create a new OrderBook instance.
*
* <p>An <code>OrderBook</code> with a maximum depth will
* never grow larger than the specified depth.
*
* @param inInstrument an <code>Instrument</code> value
* @param inMaxDepth an <code>int</code> instance
* @throws IllegalArgumentException if the given depth is invalid
*/
OrderBook(Instrument inInstrument,
int inMaxDepth)
{
if(inInstrument == null) {
throw new NullPointerException();
}
validateMaximumBookDepth(inMaxDepth);
mInstrument = inInstrument;
mAskBook = new BookCollection<AskEvent>(inMaxDepth);
mBidBook = new BookCollection<BidEvent>(inMaxDepth);
mMaxDepth = inMaxDepth;
}
/**
* Returns a string padded on the right with the given character to the given size.
*
* @param inBase a <code>String</code> value containing the string to pad
* @param inSize an <code>int</code> value containing the total number of characters to return
* @param inPadChar a <code>char</code> value containing the character with which to pad, if necessary
* @return a <code>String</code> value containing the passed string padded, if necessary, with the passed char to reach the passed size
*/
private static String pad(String inBase,
int inSize,
char inPadChar)
{
StringBuilder output = new StringBuilder(inBase);
for(int i=output.length();i<inSize;i++) {
output.append(inPadChar);
}
return output.toString();
}
/**
* Checks the given event to make sure it is appropriate to add to the <code>OrderBook</code>.
*
* @param inEvent a <code>QuoteEvent</code> value
* @throws IllegalArgumentException if the event's symbol does not match the book's symbol
*/
private void checkEvent(QuoteEvent inEvent)
{
if(!inEvent.getInstrument().equals(getInstrument())) {
throw new IllegalArgumentException(INSTRUMENT_DOES_NOT_MATCH_ORDER_BOOK_INSTRUMENT.getText(inEvent.getInstrument(),
getInstrument()));
}
}
/**
* Adds the given event to the order book.
*
* @param inEvent a <code>QuoteEvent</code> value
* @return a <code>QuoteEvent</code> value containing the event displaced by the new event or null
*/
private QuoteEvent addEvent(QuoteEvent inEvent)
{
if(inEvent instanceof BidEvent) {
return mBidBook.add((BidEvent)inEvent);
} else if(inEvent instanceof AskEvent) {
return mAskBook.add((AskEvent)inEvent);
}
throw new UnsupportedOperationException();
}
/**
* Updates the given event on the order book if it is already present.
*
* <p>If the given event does not exist in the order book, this method does nothing.
*
* @param inEvent a <code>BidAskEvent</code> value
*/
private void changeEvent(QuoteEvent inEvent)
{
if(inEvent instanceof BidEvent) {
mBidBook.change((BidEvent)inEvent);
} else if(inEvent instanceof AskEvent) {
mAskBook.change((AskEvent)inEvent);
}
}
/**
* Removes the given event from the order book.
*
* <p>If the given event does not exist in the order book, this method does nothing.
*
* @param inEvent a <code>BidAskEvent</code> value
*/
private void removeEvent(QuoteEvent inEvent)
{
if(inEvent instanceof BidEvent) {
mBidBook.remove((BidEvent)inEvent);
} else if(inEvent instanceof AskEvent) {
mAskBook.remove((AskEvent)inEvent);
}
}
/**
* Stores the orders of one side of a book.
*
* @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a>
* @version $Id: OrderBook.java 16852 2014-03-05 17:07:47Z colin $
* @since 0.6.0
*/
@ClassVersion("$Id: OrderBook.java 16852 2014-03-05 17:07:47Z colin $") //$NON-NLS-1$
private static class BookCollection<E extends QuoteEvent>
{
/**
* the set of events that make of the book
*/
private final Set<E> mBook;
/**
* if a max book depth is set, this collection tracks the order events were added to the book - note that this collection must not
* be used to retrieve values for events - use {@link #mBook} instead
*/
private final Deque<E> mBookOrder;
/**
* the maximum depth of the book. if set to {@link OrderBook#UNLIMITED_DEPTH}, the book has no maximum depth.
*/
private final int mMaxDepth;
/**
* Create a new BookCollection instance.
*
* @param inMaxDepth an <code>int</code> value indicating the maximum depth of the book or {@link OrderBook#UNLIMITED_DEPTH} if the book is to have no depth limit
*/
private BookCollection(int inMaxDepth)
{
mMaxDepth = inMaxDepth;
if(inMaxDepth == UNLIMITED_DEPTH) {
mBook = new HashSet<E>();
mBookOrder = null;
} else {
// an order book will generally fill to its max depth, so pre-allocate the memory, if a max depth is set
mBook = new HashSet<E>(mMaxDepth);
mBookOrder = new LinkedList<E>();
}
}
/**
* Adds the given event to the book.
*
* @param inEvent an <code>E</code> value to add to the book
* @return an <code>E</code> value if the incoming event displaced an existing event because the book is already at its maximum depth or null if no event was displaced
*/
private synchronized E add(E inEvent)
{
// holds the value to return, if any
E oldestEvent = null;
// if we are tracking depth, extra work needs to be done
if(mMaxDepth != UNLIMITED_DEPTH) {
// add the new event to the front of the deque tracking event age
mBookOrder.addFirst(inEvent);
// check to see if the max depth will be exceeded
if(mBook.size() >= mMaxDepth) {
// remove the event on the back of the deque (oldest event)
oldestEvent = mBookOrder.removeLast();
// remove the same event from the actual book
mBook.remove(oldestEvent);
}
}
// add the new event to the book
mBook.add(inEvent);
// return the displaced event, if any
return oldestEvent;
}
/**
* Updates the given event, if present.
*
* <p>If the event is not present in the order book, this method does nothing. Executing
* this method does not change the age of the order on the book.
*
* @param inEvent an <code>E</code> value
*/
private synchronized void change(E inEvent)
{
if(mBook.contains(inEvent)) {
mBook.remove(inEvent);
mBook.add(inEvent);
}
}
/**
* Removes the given event from the book, if present.
*
* <p>If the event is not present in the order book, this method does nothing.
*
* @param inEvent an <code>E</code> value
*/
private synchronized void remove(E inEvent)
{
// remove the event from the book. this operation is O(1).
mBook.remove(inEvent);
// if the book has a maximum depth,
if(mMaxDepth != UNLIMITED_DEPTH) {
// this operation is O(n) which is clearly not ideal, but you'd get the same behavior in a LinkedHashMap which is what this class essentially
// emulates. further, if an order book has a depth set, by definition this suggests a maximum value of n which is small enough that this
// operation is not likely to be overly punitive. so there.
mBookOrder.remove(inEvent);
}
}
/**
* Returns a view of the book sorted by the given comparator.
*
* @param inComparator a <code>Comparator<BidAskEvent></code> value to sort the list
* @return a <code>List<E></code> value
*/
private synchronized List<E> getSortedView(Comparator<QuoteEvent> inComparator)
{
List<E> events = new ArrayList<E>();
for(E event : mBook) {
events.add(event);
}
// before you ask, the reason why the book has to be re-sorted in a list with a special comparator as opposed to
// using a TreeSet with the comparator baked in to store the book in the first place, is that the book needs
// to use the messageId on the event as the key in order to support CRUD properly. the results of the book, though
// are displayed sorted in a user-sensible order.
Collections.sort(events,
inComparator);
return Collections.unmodifiableList(events);
}
}
/**
* the instrument for this book
*/
private final Instrument mInstrument;
/**
* the ask side of the book
*/
private final BookCollection<AskEvent> mAskBook;
/**
* the bid side of the book
*/
private final BookCollection<BidEvent> mBidBook;
/**
* the maximum depth of the order book
*/
private final int mMaxDepth;
}