package org.marketcetera.marketdata; import static java.math.BigDecimal.TEN; import static org.junit.Assert.*; import java.math.BigDecimal; import java.util.*; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.marketcetera.core.LoggerConfiguration; import org.marketcetera.event.*; import org.marketcetera.event.impl.QuoteEventBuilder; import org.marketcetera.module.ExpectedFailure; import org.marketcetera.trade.Equity; /* $License$ */ /** * Tests {@link OrderBook}. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: OrderBookTest.java 16154 2012-07-14 16:34:05Z colin $ * @since 0.6.0 */ public class OrderBookTest { /** * test symbol */ private final Equity symbol = new Equity("GOOG"); /** * test order book (reset each test) */ private OrderBook book; /** * test exchange */ private final String exchange = "TEST"; /** * collection used to track expected values for bids */ private final QuantityTupleList<BidEvent> bids = new QuantityTupleList<BidEvent>(); /** * collection used to track expected values for asks */ private final QuantityTupleList<AskEvent> asks = new QuantityTupleList<AskEvent>(); /** * Run once before all tests. * * @throws Exception if an error occurs */ @BeforeClass public static void once() throws Exception { LoggerConfiguration.logSetup(); } /** * Run before each test. * * @throws Exception if an error occurs */ @Before public void setup() throws Exception { bids.clear(); asks.clear(); book = new OrderBook(symbol); } /** * Tests the behavior of {@link OrderBook#equals(Object)} and {@link OrderBook#hashCode()}. * * @throws Exception if an error occurs */ @Test public void equalsAndHashCode() throws Exception { Equity otherSymbol = new Equity("YHOO"); Equity duplicateSymbol = new Equity("GOOG"); assertEquals(symbol, duplicateSymbol); assertFalse(symbol.equals(otherSymbol)); OrderBook book1 = new OrderBook(symbol); // test easy ones assertFalse(book1.equals(null)); assertFalse(book1.equals(this)); assertEquals(book1, book1); // now, ones with the same class OrderBook book2 = new OrderBook(otherSymbol); OrderBook book3 = new OrderBook(duplicateSymbol); assertFalse(book1.equals(book2)); assertFalse(book1.hashCode() == book2.hashCode()); assertFalse(book2.equals(book1)); assertFalse(book2.hashCode() == book1.hashCode()); assertEquals(book1, book3); assertEquals(book3, book1); assertEquals(book1.hashCode(), book3.hashCode()); assertEquals(book3.hashCode(), book1.hashCode()); } /** * Tests the order book constructors. * * @throws Exception if an error occurs */ @Test public void bookConstruction() throws Exception { new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { new OrderBook(null); } }; new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { new OrderBook(null, 1); } }; new ExpectedFailure<IllegalArgumentException>() { @Override protected void run() throws Exception { new OrderBook(symbol, -2); } }; new ExpectedFailure<IllegalArgumentException>() { @Override protected void run() throws Exception { new OrderBook(symbol, 0); } }; verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, new OrderBook(symbol)); verifyBook(symbol, bids, asks, 10, new OrderBook(symbol, 10)); } /** * Tests book processing of a series of bids, asks, and trades. * * @throws Exception if an error occurs */ @Test public void bookAdds() throws Exception { // bid book and ask book are both empty verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); // create an ask AskEvent ask1 = EventTestBase.generateEquityAskEvent(symbol, exchange); book.process(ask1); asks.add(ask1); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); // create a new ask of lesser worth (higher price) AskEvent ask2 = EventTestBase.generateEquityAskEvent(symbol, exchange, ask1.getPrice().add(TEN)); book.process(ask2); asks.add(ask2); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); // create a new ask of greater worth (lower price) AskEvent ask3 = EventTestBase.generateEquityAskEvent(symbol, exchange, ask1.getPrice().subtract(TEN)); book.process(ask3); asks.add(ask3); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); // add a bid BidEvent bid1 = EventTestBase.generateEquityBidEvent(symbol, exchange); book.process(bid1); bids.add(bid1); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); // create a new bid of lesser worth (lower price) BidEvent bid2 = EventTestBase.generateEquityBidEvent(symbol, exchange, bid1.getPrice().subtract(TEN)); book.process(bid2); bids.add(bid2); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); // create a new bid of greater worth (higher price) BidEvent bid3 = EventTestBase.generateEquityBidEvent(symbol, exchange, bid1.getPrice().add(TEN)); book.process(bid3); bids.add(bid3); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); } /** * Tests the order book's ability to correctly process event change and delete actions. * * @throws Exception if an error occurs */ @Test public void bookChangesAndDeletes() throws Exception { verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); // add an ask AskEvent ask1 = EventTestBase.generateEquityAskEvent(symbol, exchange); asks.add(ask1); book.process(ask1); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); BidEvent bid1 = EventTestBase.generateEquityBidEvent(symbol, exchange); bids.add(bid1); book.process(bid1); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); // delete each event in turn AskEvent ask1Killer = QuoteEventBuilder.delete(ask1); asks.clear(); book.process(ask1Killer); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); BidEvent bid1Killer = QuoteEventBuilder.delete(bid1); bids.clear(); book.process(bid1Killer); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); // try to delete from empty book (matching bid/ask doesn't exist) book.process(ask1Killer); book.process(bid1Killer); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); // add the bid and ask back book.process(ask1); book.process(bid1); asks.add(ask1); bids.add(bid1); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); QuoteEventBuilder.equityAskEvent(); // change the ask AskEvent askChange = QuoteEventBuilder.change(ask1, ask1.getTimestamp(), ask1.getSize().add(TEN)); asks.clear(); asks.add(askChange); book.process(askChange); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); QuoteEventBuilder.equityBidEvent(); // change the bid BidEvent bidChange = QuoteEventBuilder.change(bid1, bid1.getTimestamp(), bid1.getSize().add(TEN)); bids.clear(); bids.add(bidChange); book.process(bidChange); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); // create changes for non-existent events AskEvent unusedAsk = EventTestBase.generateEquityAskEvent(symbol, exchange); QuoteEventBuilder.equityAskEvent(); book.process(QuoteEventBuilder.change(unusedAsk, unusedAsk.getTimestamp(), unusedAsk.getSize().add(TEN))); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); BidEvent unusedBid = EventTestBase.generateEquityBidEvent(symbol, exchange); QuoteEventBuilder.equityBidEvent(); book.process(QuoteEventBuilder.change(unusedBid, unusedBid.getTimestamp(), unusedBid.getSize().add(TEN))); verifyBook(symbol, bids, asks, OrderBook.UNLIMITED_DEPTH, book); } /** * Tests bad values passed to {@link OrderBook#process(QuoteEvent)}. * * @throws Exception if an error occurs */ @Test public void processBadEvents() throws Exception { new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { book.process(null); } }; final AskEvent badAsk = EventTestBase.generateEquityAskEvent(new Equity("METC"), exchange); assertFalse(badAsk.getInstrument().equals(symbol)); new ExpectedFailure<IllegalArgumentException>() { @Override protected void run() throws Exception { book.process(badAsk); } }; } /** * Tests the behavior of order books with a defined maximum depth. * * @throws Exception if an error occurs */ @Test public void bookDepths() throws Exception { // events removed from a book are oldest not best, keep that in mind book = new OrderBook(symbol, 2); // create three asks with a measurable difference in their timestamps // to prove that the pruning technique is by age instead of value, make the oldest ask the best (lowest price) AskEvent ask1 = EventTestBase.generateEquityAskEvent(symbol, exchange); Thread.sleep(250); AskEvent ask2 = EventTestBase.generateEquityAskEvent(symbol, exchange, ask1.getPrice().add(TEN)); Thread.sleep(250); AskEvent ask3 = EventTestBase.generateEquityAskEvent(symbol, exchange, ask2.getPrice().add(TEN)); // add the ask events to the book book.process(ask1); asks.add(ask1); verifyBook(symbol, bids, asks, 2, book); book.process(ask2); asks.add(ask2); verifyBook(symbol, bids, asks, 2, book); // so far, so good, the order book is now at max size // add the third ask, which will trigger the pruning algorithm // the pruned event will be the oldest (ask1) not the worst (ask3) book.process(ask3); asks.add(ask3); asks.remove(ask1); verifyBook(symbol, bids, asks, 2, book); // verify the same behavior for bids BidEvent bid1 = EventTestBase.generateEquityBidEvent(symbol, exchange); Thread.sleep(250); BidEvent bid2 = EventTestBase.generateEquityBidEvent(symbol, exchange, bid1.getPrice().subtract(TEN)); Thread.sleep(250); BidEvent bid3 = EventTestBase.generateEquityBidEvent(symbol, exchange, bid2.getPrice().subtract(TEN)); // add the bid events to the book book.process(bid1); bids.add(bid1); verifyBook(symbol, bids, asks, 2, book); book.process(bid2); bids.add(bid2); verifyBook(symbol, bids, asks, 2, book); // so far, so good, the order book is now at max size // add the third Bid, which will trigger the pruning algorithm // the pruned event will be the oldest (Bid1) not the worst (Bid3) book.process(bid3); bids.add(bid3); bids.remove(bid1); verifyBook(symbol, bids, asks, 2, book); // verify deletion behavior on a limited-size book AskEvent askKiller = QuoteEventBuilder.delete(ask2); asks.remove(ask2); book.process(askKiller); verifyBook(symbol, bids, asks, 2, book); BidEvent bidKiller = QuoteEventBuilder.delete(bid2); bids.remove(bid2); book.process(bidKiller); verifyBook(symbol, bids, asks, 2, book); } @Test public void bookPerformance() throws Exception { Random random = new Random(System.nanoTime()); // the purpose of this test is to measure the performance of the order book as the depth increases OrderBook book = new OrderBook(symbol); // generate 10000 unique events List<QuoteEvent> bids = new ArrayList<QuoteEvent>(); QuoteEventBuilder<BidEvent> builder = QuoteEventBuilder.bidEvent(symbol); builder.withSize(TEN) .withExchange("some exchange") .withQuoteDate(DateUtils.dateToString(new Date())); for(int i=0;i<10000;i++) { builder.withMessageId(i+1) .withPrice(new BigDecimal(random.nextLong())); bids.add(builder.create()); } BigDecimal firstAggregate = BigDecimal.ZERO; BigDecimal lastAggregate = BigDecimal.ZERO; long counter = 0; for(QuoteEvent quote : bids) { counter += 1; long start = System.nanoTime(); book.process(quote); book.getBidBook(); long complete = System.nanoTime() - start; if(counter <= 100) { firstAggregate = firstAggregate.add(new BigDecimal(complete)); } if(counter >= 9900) { lastAggregate = lastAggregate.add(new BigDecimal(complete)); } } System.out.println("First: " + firstAggregate.divide(new BigDecimal(100))); System.out.println("Last: " + lastAggregate.divide(new BigDecimal(100))); } /** * Verifies that the given {@link OrderBook} contains the given expected values. * * @param inExpectedInstrument * @param inExpectedBids * @param inExpectedAsks * @param inExpectedMaxDepth * @param inActualBook * @throws Exception */ private void verifyBook(Equity inExpectedInstrument, QuantityTupleList<BidEvent> inExpectedBids, QuantityTupleList<AskEvent> inExpectedAsks, int inExpectedMaxDepth, OrderBook inActualBook) throws Exception { inExpectedAsks.sort(QuantityTuple.PriceComparator.ASCENDING_EQUITY); inExpectedBids.sort(QuantityTuple.PriceComparator.DESCENDING_EQUITY); List<QuantityTuple> convertedBids = convertEvents(inActualBook.getBidBook()); List<QuantityTuple> convertedAsks = convertEvents(inActualBook.getAskBook()); assertEquals(inExpectedInstrument, inActualBook.getInstrument()); assertEquals(inExpectedBids.getList(), convertedBids); assertEquals(inExpectedAsks.getList(), convertedAsks); assertEquals(inExpectedMaxDepth, inActualBook.getMaxDepth()); TopOfBookEvent top = inActualBook.getTopOfBook(); DepthOfBookEvent depth = inActualBook.getDepthOfBook(); List<QuantityTuple> convertedDepthBids = convertEvents(depth.getBids()); List<QuantityTuple> convertedDepthAsks = convertEvents(depth.getAsks()); if(inExpectedBids.isEmpty()) { assertNull(top.getBid()); assertTrue(depth.getBids().isEmpty()); } else { assertEquals(inExpectedBids.get(0), convertEvent(top.getBid())); assertEquals(inExpectedBids.getList(), convertedDepthBids); } if(inExpectedAsks.isEmpty()) { assertNull(top.getAsk()); assertTrue(depth.getAsks().isEmpty()); } else { assertEquals(inExpectedAsks.get(0), convertEvent(top.getAsk())); assertEquals(inExpectedAsks.getList(), convertedDepthAsks); } } /** * Converts the given {@link QuoteEvent} values to {@link QuantityTuple} values. * * @param inEvents a <code>List<? extends Event></code> value * @return a <code>List<QuantityTuple></code>value */ public static List<QuantityTuple> convertEvents(List<? extends Event> inEvents) { List<QuantityTuple> result = new ArrayList<QuantityTuple>(); for(Event event : inEvents) { if(event instanceof MarketDataEvent) { result.add(convertEvent((MarketDataEvent)event)); } } return result; } /** * Convert the given {@link MarketDataEvent} to a {@link QuantityTuple} value. * * @param inEvent a <code>MarketDataEvent</code> value * @return a <code>QuantityTuple</code> value */ public static QuantityTuple convertEvent(MarketDataEvent inEvent) { if(inEvent == null) { return null; } Class<? extends MarketDataEvent> type = null; if(inEvent instanceof AskEvent) { type = AskEvent.class; } else if(inEvent instanceof BidEvent) { type = BidEvent.class; } else if(inEvent instanceof TradeEvent) { type = TradeEvent.class; } else { fail("Add support for " + inEvent); } return new QuantityTuple(inEvent.getPrice(), inEvent.getSize(), type); } /** * Wrapper around a {@link QuantityTuple} {@link List} that accepts {@link QuoteEvent} inputs. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: OrderBookTest.java 16154 2012-07-14 16:34:05Z colin $ * @since 1.5.0 */ private static class QuantityTupleList<E extends QuoteEvent> { /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("QuantityTupleList [tuples=").append(tuples).append("]"); return builder.toString(); } /** * the list of quantitytuple values */ private final List<QuantityTuple> tuples = new ArrayList<QuantityTuple>(); /** * Adds an event to the list. * * @param inEvent an <code>E</code> value */ private void add(E inEvent) { tuples.add(convertEvent(inEvent)); } /** * Removes the given <code>E</code> value from the list. * * @param inEvent an <code>E</code> value */ private void remove(E inEvent) { tuples.remove(convertEvent(inEvent)); } /** * Sorts the list with the given <code>Comparator</code>. * * @param inComparator a <code>Comparator<QuantityTuple></code> value */ private void sort(Comparator<QuantityTuple> inComparator) { Collections.sort(tuples, inComparator); } /** * Gets the <code>QuantityTuple</code> at the specified index. * * @param inIndex an <code>int</code> value * @return a <code>QuantityTuple</code> value */ private QuantityTuple get(int inIndex) { return tuples.get(inIndex); } /** * Indicates if the list is empty or not. * * @return a <code>boolean</code> value */ private boolean isEmpty() { return tuples.isEmpty(); } /** * Clears all objects from the list. */ private void clear() { tuples.clear(); } /** * Returns the list. * * @return a <code>List<QuantityTuple></code> value */ private List<QuantityTuple> getList() { return tuples; } } }