package org.marketcetera.marketdata; import static org.junit.Assert.*; import java.math.BigDecimal; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.marketcetera.core.LoggerConfiguration; import org.marketcetera.core.publisher.ISubscriber; import org.marketcetera.event.*; import org.marketcetera.event.impl.QuoteEventBuilder; import org.marketcetera.event.impl.TradeEventBuilder; import org.marketcetera.marketdata.SimulatedExchange.Token; import org.marketcetera.marketdata.SimulatedExchange.TopOfBook; import org.marketcetera.module.ExpectedFailure; import org.marketcetera.options.ExpirationType; import org.marketcetera.trade.*; import org.marketcetera.trade.Currency; import org.marketcetera.util.test.CollectionAssert; import org.marketcetera.util.test.TestCaseBase; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Multimap; /* $License$ */ /** * Tests {@link SimulatedExchange}. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchangeTest.java 16395 2012-12-10 16:29:14Z colin $ * @since 1.5.0 */ public class SimulatedExchangeTest extends TestCaseBase { private SimulatedExchange exchange; private final Equity metc = new Equity("METC"); private final Equity goog = new Equity("GOOG"); private final Option metc1Put = new Option(metc.getSymbol(), "20100319", EventTestBase.generateDecimalValue(), OptionType.Put); private final Option metc1Call = new Option(metc1Put.getSymbol(), metc1Put.getExpiry(), metc1Put.getStrikePrice(), OptionType.Call); private final Option metc2Put = new Option(metc.getSymbol(), "20110319", EventTestBase.generateDecimalValue(), OptionType.Put); private final Option metc2Call = new Option(metc2Put.getSymbol(), metc2Put.getExpiry(), metc2Put.getStrikePrice(), OptionType.Call); private final Option goog1Put = new Option(goog.getSymbol(), "20100319", EventTestBase.generateDecimalValue(), OptionType.Put); private final Option goog1Call = new Option(goog1Put.getSymbol(), goog1Put.getExpiry(), goog1Put.getStrikePrice(), OptionType.Call); private final Future brn201212 = new Future("BRN", FutureExpirationMonth.DECEMBER, 2012); private final Currency testCCY = new Currency("USD","INR","",""); private final Currency anotherCCY = new Currency("USD","GBP","",""); private BidEvent bid; private AskEvent ask; private BidEvent bidCCY; private AskEvent askCCY; private static final AtomicLong counter = new AtomicLong(0); /** * Executed once before all tests. * * @throws Exception if an error occurs */ @BeforeClass public static void once() throws Exception { LoggerConfiguration.logSetup(); } /** * Executed before each test. * * @throws Exception if an error occurs */ @Before public void setup() throws Exception { exchange = new SimulatedExchange("Test exchange", "TEST"); assertFalse(metc.equals(goog)); bid = EventTestBase.generateEquityBidEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange.getCode(), new BigDecimal("100"), new BigDecimal("1000")); // intentionally creating a large spread to make sure no trades get executed ask = EventTestBase.generateEquityAskEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange.getCode(), new BigDecimal("150"), new BigDecimal("500")); bidCCY = EventTestBase.generateCurrencyBidEvent(testCCY, new BigDecimal("150")); // intentionally creating a large spread to make sure no trades get executed askCCY = EventTestBase.generateCurrencyAskEvent(testCCY, new BigDecimal("500")); } /** * Execute after each test. * * @throws Exception if an error occurs */ @After public void teardown() throws Exception { try { exchange.stop(); } catch (Exception e) {} } /** * Tests starting and stopping exchange already started and stopped. * * @throws Exception if an error occurs */ @Test public void redundantStartAndStop() throws Exception { new ExpectedFailure<IllegalStateException>() { @Override protected void run() throws Exception { exchange.stop(); } }; exchange.start(); new ExpectedFailure<IllegalStateException>() { @Override protected void run() throws Exception { exchange.start(); } }; } /** * Tests the <code>SimulatedExchange</code> constructors. * * @throws Exception if an error occurs */ @Test public void constructors() throws Exception { final String name = "name"; final String code = "code"; new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { new SimulatedExchange(null, code); } }; new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { new SimulatedExchange(null, code, 1); } }; new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { new SimulatedExchange(name, null); } }; new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { new SimulatedExchange(name, null, 1); } }; new ExpectedFailure<IllegalArgumentException>() { @Override protected void run() throws Exception { new SimulatedExchange(name, code, -2); } }; new ExpectedFailure<IllegalArgumentException>() { @Override protected void run() throws Exception { new SimulatedExchange(name, code, 0); } }; verifyExchange(new SimulatedExchange(name, code), name, code); verifyExchange(new SimulatedExchange(name, code, 100), name, code); verifyExchange(new SimulatedExchange(name, code, OrderBook.UNLIMITED_DEPTH), name, code); } /** * Tests snapshot requests. * * @throws Exception if an error occurs */ @Test public void snapshots() throws Exception { new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { exchange.getDepthOfBook(null); } }; new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { exchange.getTopOfBook(null); } }; new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { exchange.getLatestTick(null); } }; // exchange is not started yet new ExpectedFailure<IllegalStateException>() { @Override protected void run() throws Exception { exchange.getDepthOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()); } }; new ExpectedFailure<IllegalStateException>() { @Override protected void run() throws Exception { exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()); } }; new ExpectedFailure<IllegalStateException>() { @Override protected void run() throws Exception { exchange.getLatestTick(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()); } }; // start the exchange with a script with only one event for each side of the book List<QuoteEvent> script = new ArrayList<QuoteEvent>(); script.add(bid); script.add(ask); exchange.start(script); // get the depth-of-book for the symbol List<AskEvent> asks = new ArrayList<AskEvent>(); asks.add(ask); List<BidEvent> bids = new ArrayList<BidEvent>(); bids.add(bid); verifySnapshots(exchange, metc, null, asks, bids, null); // re-execute the same query (book already exists, make sure we're reading from the already existing book) verifySnapshots(exchange, metc, null, asks, bids, null); // execute a request for an empty book verifySnapshots(exchange, goog, null, new ArrayList<AskEvent>(), new ArrayList<BidEvent>(), null); exchange.stop(); // start the exchange again in scripted mode, this time with events in opposition to each other script.add(EventTestBase.generateEquityBidEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange.getCode(), ask.getPrice(), ask.getSize())); script.add(EventTestBase.generateEquityAskEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange.getCode(), bid.getPrice(), bid.getSize())); exchange.start(script); // verify that the book is empty (but there should be an existing trade) verifySnapshots(exchange, metc, null, new ArrayList<AskEvent>(), new ArrayList<BidEvent>(), EventTestBase.generateEquityTradeEvent(1, 1, metc, exchange.getCode(), bid.getPrice(), bid.getSize())); exchange.stop(); // restart exchange in random mode exchange.start(); // books are empty doRandomBookCheck(exchange, metc, null); // re-execute (this time the book exists) doRandomBookCheck(exchange, metc, null); } /** * Tests snapshot requests for currency. * * @throws Exception if an error occurs */ @Test public void snapshotsWithCurrency() throws Exception { // exchange is not started yet new ExpectedFailure<IllegalStateException>() { @Override protected void run() throws Exception { exchange.getDepthOfBook(ExchangeRequestBuilder.newRequest().withInstrument(testCCY).create()); } }; new ExpectedFailure<IllegalStateException>() { @Override protected void run() throws Exception { exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(testCCY).create()); } }; new ExpectedFailure<IllegalStateException>() { @Override protected void run() throws Exception { exchange.getLatestTick(ExchangeRequestBuilder.newRequest().withInstrument(testCCY).create()); } }; // start the exchange with a script with only one event for each side of the book List<QuoteEvent> script = new ArrayList<QuoteEvent>(); script.add(bidCCY); script.add(askCCY); exchange.start(script); // get the depth-of-book for the symbol List<AskEvent> asks = new ArrayList<AskEvent>(); asks.add(askCCY); List<BidEvent> bids = new ArrayList<BidEvent>(); bids.add(bidCCY); verifySnapshots(exchange, testCCY, null, asks, bids, null); // re-execute the same query (book already exists, make sure we're reading from the already existing book) verifySnapshots(exchange, testCCY, null, asks, bids, null); // execute a request for an empty book verifySnapshots(exchange, anotherCCY, null, new ArrayList<AskEvent>(), new ArrayList<BidEvent>(), null); exchange.stop(); // start the exchange again in scripted mode, this time with events in opposition to each other script.add(EventTestBase.generateEquityBidEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange.getCode(), ask.getPrice(), ask.getSize())); script.add(EventTestBase.generateEquityAskEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange.getCode(), bid.getPrice(), bid.getSize())); exchange.start(script); // verify that the book is empty (but there should be an existing trade) verifySnapshots(exchange, metc, null, new ArrayList<AskEvent>(), new ArrayList<BidEvent>(), EventTestBase.generateEquityTradeEvent(1, 1, metc, exchange.getCode(), bid.getPrice(), bid.getSize())); exchange.stop(); // restart exchange in random mode exchange.start(); // books are empty doRandomBookCheck(exchange, metc, null); // re-execute (this time the book exists) doRandomBookCheck(exchange, metc, null); } /** * Tests snapshots using options instead of equities. * * <p>Note that this method doesn't have to re-execute all * the equity tests - most of the code is identical. * * @throws Exception if an unexpected error occurs */ @Test public void snapshotWithOptions() throws Exception { List<QuoteEvent> script = new ArrayList<QuoteEvent>(); // set up a book for a few options in a chain BidEvent bid1 = EventTestBase.generateOptionBidEvent(metc1Put, metc, exchange.getCode()); BidEvent bid2 = EventTestBase.generateOptionBidEvent(metc1Call, metc, exchange.getCode()); BidEvent bid3 = EventTestBase.generateOptionBidEvent(metc2Put, metc, exchange.getCode()); BidEvent bid4 = EventTestBase.generateOptionBidEvent(metc2Call, metc, exchange.getCode()); script.add(bid1); script.add(bid2); script.add(bid3); script.add(bid4); // set up a few asks, too QuoteEventBuilder<AskEvent> askBuilder = QuoteEventBuilder.optionAskEvent(); askBuilder.withExchange(exchange.getCode()) .withExpirationType(ExpirationType.AMERICAN) .withQuoteDate(DateUtils.dateToString(new Date())) .withUnderlyingInstrument(metc); askBuilder.withInstrument(metc1Put); // create an ask that is more than the bid to prevent a trade occurring (keeps the top populated) askBuilder.withPrice(bid1.getPrice().add(BigDecimal.ONE)).withSize(bid1.getSize()); AskEvent ask1 = askBuilder.create(); script.add(ask1); // and an ask that does cause a trade askBuilder.withInstrument(metc2Put); askBuilder.withPrice(bid3.getPrice()).withSize(bid3.getSize()); AskEvent ask2 = askBuilder.create(); script.add(ask2); // there should now be books for the underlying (metc) and 4 entries in the chain (metc1Put, metc1Call, metc2Put, and metc2Call) exchange.start(script); // verify the state of the options verifySnapshots(exchange, metc1Put, metc, Arrays.asList(new AskEvent[] { ask1 } ), Arrays.asList(new BidEvent[] { bid1 } ), null); verifySnapshots(exchange, metc1Call, metc, Arrays.asList(new AskEvent[] { } ), Arrays.asList(new BidEvent[] { bid2 } ), null); TradeEvent trade = TradeEventBuilder.tradeEvent(metc2Put).withExchange(bid3.getExchange()) .withExpirationType(((OptionEvent)bid3).getExpirationType()) .withPrice(bid3.getPrice()) .withSize(bid3.getSize()) .withTradeDate(bid3.getQuoteDate()) .withUnderlyingInstrument(metc).create(); verifySnapshots(exchange, metc2Put, metc, Arrays.asList(new AskEvent[] { } ), Arrays.asList(new BidEvent[] { } ), trade); verifySnapshots(exchange, metc2Call, metc, Arrays.asList(new AskEvent[] { } ), Arrays.asList(new BidEvent[] { bid4 } ), null); // generate expected results for verifying snapshots for the underlying instrument Map<Instrument,InstrumentState> expectedResults = new HashMap<Instrument,InstrumentState>(); expectedResults.put(metc1Put, new InstrumentState(Arrays.asList(new BidEvent[] { bid1 }), Arrays.asList(new AskEvent[] { ask1 }), null)); expectedResults.put(metc1Call, new InstrumentState(Arrays.asList(new BidEvent[] { bid2 }), Arrays.asList(new AskEvent[] { }), null)); expectedResults.put(metc2Put, new InstrumentState(Arrays.asList(new BidEvent[] { }), Arrays.asList(new AskEvent[] { }), trade)); expectedResults.put(metc2Call, new InstrumentState(Arrays.asList(new BidEvent[] { bid4 }), Arrays.asList(new AskEvent[] { }), null)); verifyUnderlyingSnapshots(exchange, metc, expectedResults); // restart exchange in random mode exchange.stop(); exchange.start(); // books are empty // start traffic for each of the options in the metc chain doRandomBookCheck(exchange, metc1Put, metc); doRandomBookCheck(exchange, metc1Call, metc); doRandomBookCheck(exchange, metc2Put, metc); doRandomBookCheck(exchange, metc2Call, metc); doRandomBookCheck(exchange, goog1Put, goog); doRandomBookCheck(exchange, goog1Call, goog); // re-execute (this time the books exist) doRandomBookCheck(exchange, metc1Put, metc); doRandomBookCheck(exchange, metc1Call, metc); doRandomBookCheck(exchange, metc2Put, metc); doRandomBookCheck(exchange, metc2Call, metc); doRandomBookCheck(exchange, goog1Put, goog); doRandomBookCheck(exchange, goog1Call, goog); exchange.stop(); } /** * Tests that the <code>FilteringSubscriber</code> correctly tracks top-of-book state for Currency * * @throws Exception if an unexpected error occurs */ @Test public void subscriptionsWithCurrency() throws Exception { List<QuoteEvent> script = new ArrayList<QuoteEvent>(); // set up a book for a few options in a chain BidEvent bid1 = EventTestBase.generateCurrencyBidEvent(testCCY, new BigDecimal(95)); BidEvent bid2 = EventTestBase.generateCurrencyBidEvent(testCCY, new BigDecimal(100)); script.add(bid1); // top1 script.add(bid2); // top2 // there are two entries for currency // add an ask for just one instrument - make sure the bid and the ask don't match QuoteEventBuilder<AskEvent> askBuilder = QuoteEventBuilder.currencyAskEvent(); askBuilder.withExchange(exchange.getCode()) .withQuoteDate(DateUtils.dateToString(new Date())) .withInstrument(testCCY) .withExchange(exchange.getCode()); askBuilder.withInstrument(bid2.getInstrument()); // create an ask that is more than the bid to prevent a trade occurring (keeps the top populated) askBuilder.withPrice(bid2.getPrice().add(BigDecimal.ONE)).withSize(bid2.getSize()); AskEvent ask1 = askBuilder.create(); script.add(ask1); // top3 // this creates a nice, two-sided top-of-book for the instrument // create a new ask for the same instrument that *won't* change the top - a new top should not be generated askBuilder.withPrice(bid2.getPrice().add(BigDecimal.TEN)).withSize(bid2.getSize()); AskEvent ask2 = askBuilder.create(); script.add(ask2); // no top4! // set up a subscriber to top-of-book for the underlying instrument metc TopOfBookSubscriber topOfBook = new TopOfBookSubscriber(); exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(testCCY).create(), topOfBook); // start the exchange exchange.start(script); exchange.stop(); // measure the tops collected by the subscriber // should have: // a top for the bid1 instrument with just bid1 // a top for the bid2 instrument with just bid2 // a top for the bid2 instrument bid2-ask1 // no new top for ask2 - a total of three tops assertEquals(3,topOfBook.getTops().size()); } /** * Tests that the <code>FilteringSubscriber</code> correctly tracks top-of-book state for * different option chain members of the same underlying instrument. * * @throws Exception if an unexpected error occurs */ @Test public void subscriptionsWithOptions() throws Exception { List<QuoteEvent> script = new ArrayList<QuoteEvent>(); // set up a book for a few options in a chain BidEvent bid1 = EventTestBase.generateOptionBidEvent(metc1Put, metc, exchange.getCode()); BidEvent bid2 = EventTestBase.generateOptionBidEvent(metc1Call, metc, exchange.getCode()); script.add(bid1); // top1 script.add(bid2); // top2 // there are two entries in the option chain for metc // add an ask for just one instrument in the option chain - make sure the bid and the ask don't match QuoteEventBuilder<AskEvent> askBuilder = QuoteEventBuilder.optionAskEvent(); askBuilder.withExchange(exchange.getCode()) .withExpirationType(ExpirationType.AMERICAN) .withQuoteDate(DateUtils.dateToString(new Date())) .withUnderlyingInstrument(metc); askBuilder.withInstrument(bid2.getInstrument()); // create an ask that is more than the bid to prevent a trade occurring (keeps the top populated) askBuilder.withPrice(bid2.getPrice().add(BigDecimal.ONE)).withSize(bid2.getSize()); AskEvent ask1 = askBuilder.create(); script.add(ask1); // top3 // this creates a nice, two-sided top-of-book for the instrument // create a new ask for the same instrument that *won't* change the top - a new top should not be generated askBuilder.withPrice(bid2.getPrice().add(BigDecimal.TEN)).withSize(bid2.getSize()); AskEvent ask2 = askBuilder.create(); script.add(ask2); // no top4! // set up a subscriber to top-of-book for the underlying instrument metc TopOfBookSubscriber topOfBook = new TopOfBookSubscriber(); exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withUnderlyingInstrument(metc).create(), topOfBook); // start the exchange exchange.start(script); exchange.stop(); // measure the tops collected by the subscriber // should have: // a top for the bid1 instrument with just bid1 // a top for the bid2 instrument with just bid2 // a top for the bid2 instrument bid2-ask1 // no new top for ask2 - a total of three tops assertEquals(3, topOfBook.getTops().size()); } /** * Tests the ability to generate symbol statistics data. * * <p>Note that testing this is made challenging by the random nature of data generation * for statistics. * * @throws Exception if an error occurs */ @Test public void statistics() throws Exception { new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { exchange.getStatistics(null); } }; // exchange not started new ExpectedFailure<IllegalStateException>() { @Override protected void run() throws Exception { exchange.getStatistics(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()); } }; // done with error conditions exchange.start(); // quantities are random, even for subsequent calls and scripted mode, but // there are some conditions we can expect the values to adhere to for(int i=0;i<25000;i++) { verifyStatistics(exchange.getStatistics(ExchangeRequestBuilder.newRequest().withInstrument(metc).create())); } for(int i=0;i<25000;i++) { verifyStatistics(exchange.getStatistics(ExchangeRequestBuilder.newRequest().withInstrument(metc1Put) .withUnderlyingInstrument(metc).create())); } } /** * Tests the ability to receive statistics in a subscription. * * @throws Exception if an error occurs */ @Test public void statisticSubscriber() throws Exception { final AllEventsSubscriber equityStream = new AllEventsSubscriber(); final AllEventsSubscriber optionStream = new AllEventsSubscriber(); exchange.getStatistics(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), equityStream); exchange.getStatistics(ExchangeRequestBuilder.newRequest().withInstrument(metc1Put) .withUnderlyingInstrument(metc).create(), optionStream); exchange.start(); MarketDataFeedTestBase.wait(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return equityStream.events.size() >= 15 && optionStream.events.size() >= 15; } }); List<MarketstatEvent> stats = new ArrayList<MarketstatEvent>(); for(Event event : equityStream.events) { if(event instanceof MarketstatEvent) { stats.add((MarketstatEvent)event); } } for(Event event : optionStream.events) { if(event instanceof MarketstatEvent) { stats.add((MarketstatEvent)event); } } verifyStatistics(stats); } /** * Tests the ability to generate symbol dividends data. * * <p>Note that testing this is made challenging by the random nature of data generation * for dividends. * * @throws Exception if an error occurs */ @Test public void dividends() throws Exception { new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { exchange.getDividends(null); } }; // exchange not started new ExpectedFailure<IllegalStateException>() { @Override protected void run() throws Exception { exchange.getDividends(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()); } }; exchange.start(); // dividends for an option new ExpectedFailure<IllegalArgumentException>() { @Override protected void run() throws Exception { exchange.getDividends(ExchangeRequestBuilder.newRequest().withInstrument(metc1Put) .withUnderlyingInstrument(metc).create()); } }; // dividends by underlying new ExpectedFailure<IllegalArgumentException>() { @Override protected void run() throws Exception { exchange.getDividends(ExchangeRequestBuilder.newRequest().withUnderlyingInstrument(metc).create()); } }; for(int i=0;i<1000;i++) { Equity e = new Equity(String.format("equity-%s", i)); verifyDividends(exchange.getDividends(ExchangeRequestBuilder.newRequest().withInstrument(e).create()), e); } } /** * Tests the ability to receive dividends in a subscription. * * @throws Exception if an error occurs */ @Test public void dividendSubscriber() throws Exception { final AllEventsSubscriber dividendStream = new AllEventsSubscriber(); exchange.start(); MarketDataFeedTestBase.wait(new Callable<Boolean>() { @Override public Boolean call() throws Exception { Equity e = new Equity("e-" + counter.incrementAndGet()); exchange.getDividends(ExchangeRequestBuilder.newRequest().withInstrument(e).create(), dividendStream); return dividendStream.events.size() >= 20; } }); // sleep for a couple of ticks to make sure we don't get extra dividends (the same dividends sent more than once) Thread.sleep(5000); Multimap<Equity,DividendEvent> dividends = LinkedListMultimap.create(); for(Event event : dividendStream.events) { DividendEvent dividendEvent = (DividendEvent)event; dividends.put(dividendEvent.getEquity(), dividendEvent); } for(Equity equity : dividends.keySet()) { verifyDividends(new ArrayList<DividendEvent>(dividends.get(equity)), equity); } } /** * Tests subscribing to market data from an exchange. * * <p>This is a very complicated test due to the problem of generating complex expected * results. Apologies in advance to anyone who has to review or modify this method. The general * approach is to compile data structures that mimic the result (top-of-book, depth-of-book, etc) * using {@link QuantityTuple} objects that mirror {@link QuoteEvent} objects. * * @throws Exception if an error occurs */ @Test public void subscriptions() throws Exception { new ExpectedFailure<NullPointerException>() { @Override protected void run() throws Exception { exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), null); } }; // generate a script with a number of bids and asks BigDecimal baseValue = new BigDecimal("100.00"); BigDecimal bidSize = new BigDecimal("100"); BigDecimal askSize = new BigDecimal("50"); List<QuoteEvent> script = new ArrayList<QuoteEvent>(); List<BidEvent> bids = new ArrayList<BidEvent>(); List<AskEvent> asks = new ArrayList<AskEvent>(); for(int i=0;i<5;i++) { bids.add(EventTestBase.generateEquityBidEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange.getCode(), baseValue.add(BigDecimal.ONE.multiply(new BigDecimal(i))), bidSize)); } for(int i=4;i>=0;i--) { asks.add(EventTestBase.generateEquityAskEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange.getCode(), baseValue.add(BigDecimal.ONE.multiply(new BigDecimal(i))), askSize)); } script.addAll(bids); script.addAll(asks); // add one outrageous ask to make the book interesting AskEvent bigAsk = EventTestBase.generateEquityAskEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange.getCode(), new BigDecimal(1000), new BigDecimal(1000)); script.add(bigAsk); // set up the subscriptions TopOfBookSubscriber topOfBook = new TopOfBookSubscriber(); exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), topOfBook); AllEventsSubscriber tick = new AllEventsSubscriber(); exchange.getLatestTick(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), tick); AllEventsSubscriber depthOfBook = new AllEventsSubscriber(); exchange.getDepthOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), depthOfBook); // start the script exchange.start(script); // we can predict that the exchange will send 10 quote adds which will result in 5 trades and // 10 quote corrections (bid/ask del/chg), 25 events altogether // verify the results // this list will hold all the expected events List<QuantityTuple> allExpectedEvents = new ArrayList<QuantityTuple>(); List<BookEntryTuple> expectedTopOfBook = new ArrayList<BookEntryTuple>(); // the first events will be the bids for(BidEvent bid : bids) { QuantityTuple convertedBid = OrderBookTest.convertEvent(bid); allExpectedEvents.add(convertedBid); expectedTopOfBook.add(new BookEntryTuple(convertedBid, null)); } /* bid | ask -----------+--------- 100 104.00 | 100 103.00 | 100 102.00 | 100 101.00 | 100 100.00 | */ // next will be the asks interleaved with trades and corrections as the books are settled after each ask allExpectedEvents.add(new QuantityTuple(new BigDecimal("104.00"), // 1st of 5 asks new BigDecimal("50"), AskEvent.class)); /* bid | ask -----------+----------- 100 104.00 | 104.00 50 100 103.00 | 100 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("104.00"), // resulting trade new BigDecimal("50"), TradeEvent.class)); allExpectedEvents.add(new QuantityTuple(new BigDecimal("104.00"), // bid correction (change) new BigDecimal("50"), BidEvent.class)); /* bid | ask -----------+----------- 50 104.00 | 104.00 50 100 103.00 | 100 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("104.00"), // ask correction (delete) new BigDecimal("50"), AskEvent.class)); /* bid | ask -----------+----------- 50 104.00 | 100 103.00 | 100 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("103.00"), // 2nd of 5 asks new BigDecimal("50"), AskEvent.class)); /* bid | ask -----------+----------- 50 104.00 | 103.00 50 100 103.00 | 100 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("103.00"), // resulting trade new BigDecimal("50"), TradeEvent.class)); allExpectedEvents.add(new QuantityTuple(new BigDecimal("104.00"), // bid correction (delete of fully consumed bid) new BigDecimal("50"), BidEvent.class)); /* bid | ask -----------+----------- 100 103.00 | 103.00 50 100 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("103.00"), // ask correction (delete of fully consumed ask) new BigDecimal("50"), AskEvent.class)); /* bid | ask -----------+----------- 100 103.00 | 100 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("102.00"), // 3rd of 5 asks new BigDecimal("50"), AskEvent.class)); /* bid | ask -----------+----------- 100 103.00 | 102.00 50 100 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("102.00"), // resulting trade new BigDecimal("50"), TradeEvent.class)); allExpectedEvents.add(new QuantityTuple(new BigDecimal("103.00"), // bid correction (change of partially consumed bid) new BigDecimal("50"), BidEvent.class)); /* bid | ask -----------+----------- 50 103.00 | 102.00 50 100 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("102.00"), // ask correction (delete of fully consumed ask) new BigDecimal("50"), AskEvent.class)); /* bid | ask -----------+----------- 50 103.00 | 100 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("101.00"), // 4th of 5 asks new BigDecimal("50"), AskEvent.class)); /* bid | ask -----------+----------- 50 103.00 | 101.00 50 100 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("101.00"), // resulting trade new BigDecimal("50"), TradeEvent.class)); allExpectedEvents.add(new QuantityTuple(new BigDecimal("103.00"), // bid correction (delete of fully consumed bid) new BigDecimal("50"), BidEvent.class)); /* bid | ask -----------+----------- 100 102.00 | 101.00 50 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("101.00"), // ask correction (delete of fully consumed ask) new BigDecimal("50"), AskEvent.class)); /* bid | ask -----------+----------- 100 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("100.00"), // 5th of 5 asks new BigDecimal("50"), AskEvent.class)); /* bid | ask -----------+----------- 100 102.00 | 100.00 50 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("100.00"), // resulting trade new BigDecimal("50"), TradeEvent.class)); allExpectedEvents.add(new QuantityTuple(new BigDecimal("102.00"), // bid correction (change of partially consumed bid) new BigDecimal("50"), BidEvent.class)); /* bid | ask -----------+----------- 50 102.00 | 100.00 50 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(new BigDecimal("100.00"), // ask correction (delete of fully consumed ask) new BigDecimal("50"), AskEvent.class)); /* bid | ask -----------+----------- 50 102.00 | 100 101.00 | 100 100.00 | */ allExpectedEvents.add(new QuantityTuple(bigAsk.getPrice(), // big ask bigAsk.getSize(), AskEvent.class)); /* bid | ask -----------+------------- 50 102.00 | 1000.00 1000 100 101.00 | 100 100.00 | */ // prepare the expected top-of-book results expectedTopOfBook.addAll(Arrays.asList(new BookEntryTuple[] { /* 100 104.00 | 104.00 50 */ new BookEntryTuple(new QuantityTuple(new BigDecimal("104.00"), new BigDecimal("100"), BidEvent.class), new QuantityTuple(new BigDecimal("104.00"), new BigDecimal("50"), AskEvent.class)), /* 50 104.00 | 104.00 50 */ new BookEntryTuple(new QuantityTuple(new BigDecimal("104.00"), new BigDecimal("50"), BidEvent.class), new QuantityTuple(new BigDecimal("104.00"), new BigDecimal("50"), AskEvent.class)), /* 50 104.00 | */ new BookEntryTuple(new QuantityTuple(new BigDecimal("104.00"), new BigDecimal("50"), BidEvent.class), null), /* 50 104.00 | 103.00 50 */ new BookEntryTuple(new QuantityTuple(new BigDecimal("104.00"), new BigDecimal("50"), BidEvent.class), new QuantityTuple(new BigDecimal("103.00"), new BigDecimal("50"), AskEvent.class)), /* 100 103.00 | 103.00 50 */ new BookEntryTuple(new QuantityTuple(new BigDecimal("103.00"), new BigDecimal("100"), BidEvent.class), new QuantityTuple(new BigDecimal("103.00"), new BigDecimal("50"), AskEvent.class)), /* 100 103.00 | */ new BookEntryTuple(new QuantityTuple(new BigDecimal("103.00"), new BigDecimal("100"), BidEvent.class), null), /* 100 103.00 | 102.00 50 */ new BookEntryTuple(new QuantityTuple(new BigDecimal("103.00"), new BigDecimal("100"), BidEvent.class), new QuantityTuple(new BigDecimal("102.00"), new BigDecimal("50"), AskEvent.class)), /* 50 103.00 | 102.00 50 */ new BookEntryTuple(new QuantityTuple(new BigDecimal("103.00"), new BigDecimal("50"), BidEvent.class), new QuantityTuple(new BigDecimal("102.00"), new BigDecimal("50"), AskEvent.class)), /* 50 103.00 | */ new BookEntryTuple(new QuantityTuple(new BigDecimal("103.00"), new BigDecimal("50"), BidEvent.class), null), /* 50 103.00 | 101.00 50 */ new BookEntryTuple(new QuantityTuple(new BigDecimal("103.00"), new BigDecimal("50"), BidEvent.class), new QuantityTuple(new BigDecimal("101.00"), new BigDecimal("50"), AskEvent.class)), /* 100 102.00 | 101.00 50 */ new BookEntryTuple(new QuantityTuple(new BigDecimal("102.00"), new BigDecimal("100"), BidEvent.class), new QuantityTuple(new BigDecimal("101.00"), new BigDecimal("50"), AskEvent.class)), /* 100 102.00 | */ new BookEntryTuple(new QuantityTuple(new BigDecimal("102.00"), new BigDecimal("100"), BidEvent.class), null), /* 100 102.00 | 100.00 50 */ new BookEntryTuple(new QuantityTuple(new BigDecimal("102.00"), new BigDecimal("100"), BidEvent.class), new QuantityTuple(new BigDecimal("100.00"), new BigDecimal("50"), AskEvent.class)), /* 50 102.00 | 100.00 50 */ new BookEntryTuple(new QuantityTuple(new BigDecimal("102.00"), new BigDecimal("50"), BidEvent.class), new QuantityTuple(new BigDecimal("100.00"), new BigDecimal("50"), AskEvent.class)), /* 50 102.00 | */ new BookEntryTuple(new QuantityTuple(new BigDecimal("102.00"), new BigDecimal("50"), BidEvent.class), null), /* 50 102.00 | 1000 1000 */ new BookEntryTuple(new QuantityTuple(new BigDecimal("102.00"), new BigDecimal("50"), BidEvent.class), new QuantityTuple(new BigDecimal("1000"), new BigDecimal("1000"), AskEvent.class)), })); // prepare the expected latest tick results List<QuantityTuple> expectedLatestTicks = new ArrayList<QuantityTuple>(); // prepare the expected depth-of-book results List<QuantityTuple> expectedDepthOfBook = new ArrayList<QuantityTuple>(); for(QuantityTuple tuple : allExpectedEvents) { if(tuple.getType().equals(TradeEvent.class)) { expectedLatestTicks.add(tuple); } else { expectedDepthOfBook.add(tuple); } } // ready to verify results verifySubscriptions(topOfBook.getTops(), expectedTopOfBook, tick.events, expectedLatestTicks, depthOfBook.events, expectedDepthOfBook); } /** * Tests that subscribers to different exchanges for the same type of data * get only the data they are supposed to. * * @throws Exception if an error occurs */ @Test public void subscriptionTargeting() throws Exception { // create two different exchanges (one already exists, of course) SimulatedExchange exchange2 = new SimulatedExchange("Test exchange 2", "TEST2"); assertFalse(exchange.equals(exchange2)); // create two different subscribers AllEventsSubscriber sub1 = new AllEventsSubscriber(); AllEventsSubscriber sub2 = new AllEventsSubscriber(); // set up the subscriptions exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), sub1); exchange2.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), sub2); // create an event targeted to the second exchange AskEvent ask2 = EventTestBase.generateEquityAskEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange2.getCode(), new BigDecimal("150"), new BigDecimal("500")); // create an event targeted to the second exchange but with the wrong symbol AskEvent ask3 = EventTestBase.generateEquityAskEvent(counter.incrementAndGet(), System.currentTimeMillis(), goog, exchange2.getCode(), new BigDecimal("150"), new BigDecimal("500")); // create two different scripts // the events that are targeted to the wrong exchange or symbol should get skipped List<QuoteEvent> script1 = new ArrayList<QuoteEvent>(); script1.add(bid); script1.add(ask2); script1.add(ask3); List<QuoteEvent> script2 = new ArrayList<QuoteEvent>(); script2.add(ask2); script2.add(bid); script2.add(ask3); // start the exchanges exchange.start(script1); exchange2.start(script2); // measure the results assertEquals(1, sub1.events.size()); assertEquals(bid, sub1.events.get(0)); assertEquals(1, sub2.events.size()); assertEquals(ask2, sub2.events.get(0)); } /** * Tests that an over-filled (according to its max depth) order book gets pruned. * * @throws Exception if an error occurs */ @Test public void bookPruning() throws Exception { // create an exchange with a small maximum depth (2) exchange = new SimulatedExchange("Test exchange", "TEST", 2); // create a script of events that will exceed the max on both sides of the book // note that the spread is intentionally large in order to prevent any trades List<QuoteEvent> script = new ArrayList<QuoteEvent>(); List<AskEvent> asks = new ArrayList<AskEvent>(); List<BidEvent> bids = new ArrayList<BidEvent>(); for(int i=0;i<3;i++) { bids.add(EventTestBase.generateEquityBidEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange.getCode(), new BigDecimal(10-i), new BigDecimal("1000"))); asks.add(EventTestBase.generateEquityAskEvent(counter.incrementAndGet(), System.currentTimeMillis(), metc, exchange.getCode(), new BigDecimal(100+i), new BigDecimal("1000"))); } script.addAll(bids); script.addAll(asks); exchange.start(script); // now, trim the oldest (first) bid and ask from each list to simulate the pruning bids.remove(0); asks.remove(0); // check the results verifyDepthOfBook(exchange.getDepthOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()), asks, bids); } /** * Tests canceling a subscription. * * @throws Exception if an error occurs */ @Test public void subscriptionCanceling() throws Exception { final AllEventsSubscriber stream1 = new AllEventsSubscriber(); final AllEventsSubscriber stream2 = new AllEventsSubscriber(); SimulatedExchange.Token t1 = exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), stream1); SimulatedExchange.Token t2 = exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), stream2); assertFalse(t1.equals(t2)); assertFalse(t1.hashCode() == t2.hashCode()); exchange.start(); // start the exchange in random mode (wait until a reasonable number of events comes in) MarketDataFeedTestBase.wait(new Callable<Boolean>() { @Override public Boolean call() throws Exception { Thread.sleep(250); return stream1.events.size() >= 10; } }); // t2 should have received at least the same number of events (won't be deterministically in // sync) MarketDataFeedTestBase.wait(new Callable<Boolean>() { @Override public Boolean call() throws Exception { Thread.sleep(250); return stream2.events.size() >= 10; } }); // both subscribers have now received at least 10 events (this shows us that they're // both receiving events) // now, cancel one subscription - note that since it's async, we can't guarantee that // no more than 10 events will come (one may have come in even while you read this // comment) t1.cancel(); // some time very shortly (certainly in the next minute), updates should stop coming in MarketDataFeedTestBase.wait(new Callable<Boolean>() { @Override public Boolean call() throws Exception { int currentCount = stream1.events.size(); // at least 2 events should come in every second, so by waiting 2.5 seconds, // we should be able to tell with a reasonable degree of confidence that // no new events are coming in Thread.sleep(2500); return stream1.events.size() == currentCount; } }); int stream1Count = stream1.events.size(); int stream2Count = stream2.events.size(); // stream2 is still receiving events, but stream1 is not MarketDataFeedTestBase.wait(new Callable<Boolean>() { @Override public Boolean call() throws Exception { Thread.sleep(250); return stream2.events.size() >= 20; } }); // the size of stream2 has grown assertTrue(stream2.events.size() >= stream2Count); // the size of stream1 has not assertEquals(stream1Count, stream1.events.size()); // cancel the same thing again just to make sure nothing flies off the handle t1.cancel(); } /** * Tests the ability to subscribe before and after the exchange starts. * * @throws Exception if an error occurs */ @Test public void subscribeBeforeAndAfterStart() throws Exception { final AllEventsSubscriber stream1 = new AllEventsSubscriber(); final AllEventsSubscriber stream2 = new AllEventsSubscriber(); exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), stream1); exchange.start(); exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), stream2); MarketDataFeedTestBase.wait(new Callable<Boolean>() { @Override public Boolean call() throws Exception { Thread.sleep(250); return stream1.events.size() >= 10 && stream2.events.size() >= 10; } }); } /** * Tests that a scripted exchange doesn't start new activity after running its * script. * * @throws Exception if an error occurs */ @Test public void otherSymbolsFromScriptedExchange() throws Exception { // start the exchange in scripted mode with two events for METC List<QuoteEvent> script = new ArrayList<QuoteEvent>(); script.add(bid); script.add(ask); exchange.start(script); // allow the exchange the opportunity to do something off the script, if it's going to Thread.sleep(5000); // verify the top for METC verifyTopOfBook(makeTopOfBook(exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create())), ask, bid); // verify that nothing's there for GOOG assertTrue(exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(goog).create()).isEmpty()); } /** * Tests that exchanges initially have closely grouped values for the same symbol. * * @throws Exception if an error occurs */ @Test public void valueSync() throws Exception { // create two extra exchanges final SimulatedExchange exchange2 = new SimulatedExchange("Test Exchange2", "TEST2"); final SimulatedExchange exchange3 = new SimulatedExchange("Test Exchange3", "TEST3"); // start all three in random mode (note that exchange-sync behavior does not apply to scripted mode) exchange.start(); exchange2.start(); exchange3.start(); // all three are ticking over, but the books are not yet populated // wait until the book is populated for METC MarketDataFeedTestBase.wait(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()).size() == 2; } }); // the book for METC has at least an ask and bid in it, grab the value List<QuoteEvent> top1 = exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()); // now, issue the same request for exchange2 MarketDataFeedTestBase.wait(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return exchange2.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()).size() == 2; } }); List<QuoteEvent> top2 = exchange2.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()); // and exchange3 MarketDataFeedTestBase.wait(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return exchange3.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()).size() == 2; } }); List<QuoteEvent> top3 = exchange3.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create()); // we cannot guarantee how many ticks (if any) have happened during the course of these // instructions. regardless, each of the exchanges has // a book for METC with at least a bid and ask. it's unlikely they're exactly the same, but // they can have varied by no more than .01/second. the absolute maximum that is possible is limited // by about 3 minutes (each of the wait calls could take just under 60 seconds without failing). // so, we'll say that the most the values could have changed is 2.50 (that's equivalent to // a bit over 4 minutes of run time with one tick/second). // the default book start values vary between (0.01,99.99), the odds of three values hitting the // same 5.00 interval randomly without sync are not large, about 1.25 in 10,000). the worst case for // this test is a false negative, that is, sync isn't working and all three books randomly start // in the same interval. since this should happen a little more often than once in 10,000, that // makes this test effective enough. assertTrue(((MarketDataEvent)top1.get(0)).getPrice().subtract(((MarketDataEvent)top2.get(0)).getPrice()).abs().intValue() < 3); assertTrue(((MarketDataEvent)top1.get(0)).getPrice().subtract(((MarketDataEvent)top3.get(0)).getPrice()).abs().intValue() < 3); } /** * Tests the output of the exchange in random mode. * * @throws Exception if an unexpected error occurs */ @Test public void randomModeOutput() throws Exception { exchange.start(); // start the exchange in random mode final AllEventsSubscriber all = new AllEventsSubscriber(); exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(metc).create(), all); // this block should actually take less than 30s - there are 2 new events/sec and every now and then // a trade will create a few extras MarketDataFeedTestBase.wait(new Callable<Boolean>(){ @Override public Boolean call() throws Exception { return all.events.size() >= 60; } }); exchange.stop(); } /** * Tests the ability of the exchange to deliver a non-zero and non-one contract size. * * @throws Exception if an unexpected error occurs */ @Test public void testFutureContractSize() throws Exception { exchange.start(); final AllEventsSubscriber all = new AllEventsSubscriber(); Token token = exchange.getTopOfBook(ExchangeRequestBuilder.newRequest().withInstrument(brn201212).create(), all); MarketDataFeedTestBase.wait(new Callable<Boolean>(){ @Override public Boolean call() throws Exception { return all.events.size() >= 10; } }); for(Event event : all.events) { assertTrue(event instanceof FutureEvent); assertEquals(100, ((FutureEvent)event).getContractSize()); } exchange.cancel(token); all.events.clear(); token = exchange.getDepthOfBook(ExchangeRequestBuilder.newRequest().withInstrument(brn201212).create(), all); MarketDataFeedTestBase.wait(new Callable<Boolean>(){ @Override public Boolean call() throws Exception { return all.events.size() >= 10; } }); for(Event event : all.events) { assertTrue(event instanceof FutureEvent); assertEquals(100, ((FutureEvent)event).getContractSize()); } exchange.cancel(token); all.events.clear(); token = exchange.getLatestTick(ExchangeRequestBuilder.newRequest().withInstrument(brn201212).create(), all); MarketDataFeedTestBase.wait(new Callable<Boolean>(){ @Override public Boolean call() throws Exception { return all.events.size() >= 10; } }); for(Event event : all.events) { assertTrue(event instanceof FutureEvent); assertEquals(100, ((FutureEvent)event).getContractSize()); } exchange.cancel(token); all.events.clear(); token = exchange.getStatistics(ExchangeRequestBuilder.newRequest().withInstrument(brn201212).create(), all); MarketDataFeedTestBase.wait(new Callable<Boolean>(){ @Override public Boolean call() throws Exception { return all.events.size() >= 10; } }); for(Event event : all.events) { assertTrue(event instanceof FutureEvent); assertEquals(100, ((FutureEvent)event).getContractSize()); } } /** * Executes a test to make sure that the given <code>Instrument</code> and underlying <code>Instrument</code> * get order books created for them. * * @param inExchange a <code>SimulatedExchange</code> value * @param inInstrument an <code>Instrument</code> value * @param inUnderlyingInstrument an <code>Instrument</code> value * @throws Exception if an unexpected error occurs */ private void doRandomBookCheck(SimulatedExchange inExchange, Instrument inInstrument, Instrument inUnderlyingInstrument) throws Exception { List<QuoteEvent> dob = inExchange.getDepthOfBook(ExchangeRequestBuilder.newRequest().withInstrument(inInstrument) .withUnderlyingInstrument(inUnderlyingInstrument).create()); // note that since the exchange was started this time in random mode we don't know exactly what the // values will be, but there should be at least one entry on each side of the book assertFalse(dob.isEmpty()); boolean foundAsk = false; boolean foundBid = false; for(Event event : dob) { if(event instanceof BidEvent) { foundBid = true; } else if(event instanceof AskEvent) { foundAsk = true; } } assertTrue(foundBid); assertTrue(foundAsk); // repeat, checking by underlying instrument only if(inUnderlyingInstrument != null) { dob = inExchange.getDepthOfBook(ExchangeRequestBuilder.newRequest().withUnderlyingInstrument(inUnderlyingInstrument).create()); assertFalse(dob.isEmpty()); foundAsk = false; foundBid = false; for(Event event : dob) { if(event instanceof BidEvent) { foundBid = true; } else if(event instanceof AskEvent) { foundAsk = true; } } assertTrue(foundBid); assertTrue(foundAsk); } } /** * Verifies the given actual subscriptions against the expected results. * * @param inActualTopOfBook a <code>List<Pair<BidEvent,AskEvent>></code> value * @param inExpectedTopOfBook a <code>List<BookEntryTuple></code> value * @param inActualTicks a <code>List<EventBase></code> value * @param inExpectedTicks a <code>List<QuantityTuple></code> value * @param inActualDepthOfBook a <code>List<EventBase></code> value * @param inExpectedDepthOfBook a <code>List<QuantityTuple></code> value * @throws Exception if an error occurs */ private void verifySubscriptions(List<TopOfBook> inActualTopOfBook, List<BookEntryTuple> inExpectedTopOfBook, List<Event> inActualTicks, List<QuantityTuple> inExpectedTicks, List<Event> inActualDepthOfBook, List<QuantityTuple> inExpectedDepthOfBook) throws Exception { // test top-of-book assertEquals(inExpectedTopOfBook.size(), inActualTopOfBook.size()); List<BookEntryTuple> actualTopOfBook = new ArrayList<BookEntryTuple>(); for(TopOfBook top : inActualTopOfBook) { actualTopOfBook.add(new BookEntryTuple(OrderBookTest.convertEvent(top.getFirstMember()), OrderBookTest.convertEvent(top.getSecondMember()))); } assertEquals(inExpectedTopOfBook, actualTopOfBook); // test latest-tick assertEquals(inExpectedTicks, OrderBookTest.convertEvents(inActualTicks)); // test depth-of-book assertEquals(inExpectedDepthOfBook.size(), inActualDepthOfBook.size()); assertEquals(inExpectedDepthOfBook, OrderBookTest.convertEvents(inActualDepthOfBook)); } /** * Verifies symbol statistical data. * * @param inStatistics a <code>List<MarketstatEvent></code> value containing the actual value * @throws Exception if an error occurs */ private void verifyStatistics(List<MarketstatEvent> inStatistics) throws Exception { for(MarketstatEvent stat : inStatistics) { assertNotNull(stat.getOpen()); assertNotNull(stat.getHigh()); assertNotNull(stat.getLow()); assertNotNull(stat.getClose()); assertNotNull(stat.getPreviousClose()); assertNotNull(stat.getVolume()); assertNotNull(stat.getCloseDate()); assertNotNull(stat.getPreviousCloseDate()); assertNotNull(stat.getTradeHighTime()); assertNotNull(stat.getTradeLowTime()); assertNotNull(stat.getOpenExchange()); assertNotNull(stat.getCloseExchange()); assertNotNull(stat.getHighExchange()); assertNotNull(stat.getLowExchange()); Instrument instrument = stat.getInstrument(); if(instrument instanceof Option) { assertNotNull(((OptionMarketstatEvent)stat).getInterestChange()); assertNotNull(((OptionMarketstatEvent)stat).getVolumeChange()); } } } /** * Verifies symbol dividends. * * @param inDividends a <code>List<Marketstatevent></code> value * @param inEquity an <code>Equity</code> value * @throws Exception if an unexpected error occurs */ private void verifyDividends(List<DividendEvent> inDividends, Equity inEquity) throws Exception { // make sure that the dividends we got match the dividends we get a second // time for the same equity assertEquals(inDividends, exchange.getDividends(ExchangeRequestBuilder.newRequest().withInstrument(inEquity).create())); if(!inDividends.isEmpty()) { assertEquals(4, inDividends.size()); DividendEvent currentDividend = inDividends.get(0); assertTrue(currentDividend.getAmount().compareTo(BigDecimal.ZERO) == 1); assertEquals("USD", currentDividend.getCurrency()); assertEquals(inEquity, currentDividend.getEquity()); Date today = new Date(); assertNotNull(currentDividend.getDeclareDate()); assertTrue(today.after(DateUtils.stringToDate(currentDividend.getDeclareDate()))); assertNotNull(currentDividend.getExecutionDate()); assertTrue(today.after(DateUtils.stringToDate(currentDividend.getExecutionDate()))); assertNotNull(currentDividend.getPaymentDate()); assertTrue(today.after(DateUtils.stringToDate(currentDividend.getPaymentDate()))); assertNotNull(currentDividend.getRecordDate()); assertTrue(today.after(DateUtils.stringToDate(currentDividend.getRecordDate()))); assertEquals(DividendFrequency.QUARTERLY, currentDividend.getFrequency()); assertEquals(DividendStatus.OFFICIAL, currentDividend.getStatus()); assertEquals(DividendType.CURRENT, currentDividend.getType()); // now check future dividends for(int counter=1;counter<=3;counter++) { DividendEvent futureDividend = inDividends.get(counter); assertTrue(futureDividend.getAmount().compareTo(BigDecimal.ZERO) == 1); assertEquals("USD", futureDividend.getCurrency()); assertEquals(inEquity, futureDividend.getEquity()); assertEquals(DividendFrequency.QUARTERLY, futureDividend.getFrequency()); assertEquals(DividendStatus.UNOFFICIAL, futureDividend.getStatus()); assertEquals(DividendType.FUTURE, futureDividend.getType()); assertNull(futureDividend.getDeclareDate()); assertNull(futureDividend.getPaymentDate()); assertNull(futureDividend.getRecordDate()); String executionDate = futureDividend.getExecutionDate(); assertNotNull(executionDate); assertTrue(today.before(DateUtils.stringToDate(executionDate))); } } } /** * Verifies that the given exchange and symbol will produce the expected snapshots. * * @param inExchange a <code>SimulatedExchange</code> value * @param inInstrument an <code>Instrument</code> value * @param inUnderlyingInstrument an <code>Instrument</code> value or <code>null</code> * @param inExpectedAsks a <code>List<AskEvent></code> value * @param inExpectedBids a <code>List<BidEvent></code> value * @throws Exception if an error occurs */ private void verifySnapshots(SimulatedExchange inExchange, Instrument inInstrument, Instrument inUnderlyingInstrument, List<AskEvent> inExpectedAsks, List<BidEvent> inExpectedBids, TradeEvent inExpectedLatestTick) throws Exception { ExchangeRequest request = ExchangeRequestBuilder.newRequest().withInstrument(inInstrument) .withUnderlyingInstrument(inUnderlyingInstrument).create(); verifyDepthOfBook(inExchange.getDepthOfBook(request), inExpectedAsks, inExpectedBids); verifyTopOfBook(makeTopOfBook(inExchange.getTopOfBook(request)), inExpectedAsks.isEmpty() ? null : inExpectedAsks.get(0), inExpectedBids.isEmpty() ? null : inExpectedBids.get(0)); assertEquals(OrderBookTest.convertEvent(inExpectedLatestTick), OrderBookTest.convertEvent(exchange.getLatestTick(request).get(0))); } /** * Verifies that the underlying instrument leads to the given expected states in the * given exchange. * * @param inExchange a <code>SimulatedExchange</code> value * @param inUnderlyingInstrument an <code>Instrument</code> value * @param inExpectedStates a <code>Map<Instrument,InstrumentState></code> value * @throws Exception if an unexpected error occurs */ private void verifyUnderlyingSnapshots(SimulatedExchange inExchange, Instrument inUnderlyingInstrument, Map<Instrument,InstrumentState> inExpectedStates) throws Exception { ExchangeRequest request = ExchangeRequestBuilder.newRequest().withUnderlyingInstrument(inUnderlyingInstrument).create(); EventOrganizer.process(inExchange.getTopOfBook(request), EventOrganizer.RequestType.TOP_OF_BOOK); EventOrganizer.process(inExchange.getDepthOfBook(request), EventOrganizer.RequestType.DEPTH_OF_BOOK); EventOrganizer.process(inExchange.getLatestTick(request), EventOrganizer.RequestType.LATEST_TICK); EventOrganizer.process(inExchange.getStatistics(request), EventOrganizer.RequestType.MARKET_STAT); // all the actual events have been collected and organized, examine them for(Map.Entry<Instrument,InstrumentState> entry : inExpectedStates.entrySet()) { InstrumentState expectedState = inExpectedStates.get(entry.getKey()); EventOrganizer organizer = EventOrganizer.organizers.remove(entry.getKey()); assertNotNull("No actual results for " + entry.getKey(), organizer); verifyTopOfBook(organizer.getTop(), expectedState.asks.isEmpty() ? null : expectedState.asks.get(0), expectedState.bids.isEmpty() ? null : expectedState.bids.get(0)); verifyDepthOfBook(organizer.depthOfBook, expectedState.asks, expectedState.bids); verifyStatistics(Arrays.asList(new MarketstatEvent[] { organizer.marketstat } )); assertEquals(OrderBookTest.convertEvent(organizer.latestTrade), OrderBookTest.convertEvent(expectedState.latestTrade)); } } /** * Creates a <code>TopOfBook</code> object from the given list. * * @param inEvents a <code>List<QuoteEvent></code> value * @return a <code>TopOfBook</code> value * @throws Exception if an unexpected error occurs */ private static TopOfBook makeTopOfBook(List<QuoteEvent> inEvents) throws Exception { assertTrue("An instrument should have a single top-of-book", inEvents.size() <= 2); BidEvent bid = null; AskEvent ask = null; if(inEvents.size() == 2) { bid = (BidEvent)inEvents.get(0); ask = (AskEvent)inEvents.get(1); } else if(inEvents.size() == 1) { Event e = inEvents.get(0); if(e instanceof BidEvent) { bid = (BidEvent)e; } else if(e instanceof AskEvent) { ask = (AskEvent)e; } else { fail("Unknown contents in top-of-book: " + e); } } return new TopOfBook(bid, ask); } /** * Verifies the given actual<code>TopOfBook</code> contains the expected values. * * @param inActualTopOfBook a <code>TopOfBook</code> value * @param inAsk a <code>AskEvent</code> value * @param inBid a <code>BidEvent</code> value * @throws Exception if an error occurs */ private void verifyTopOfBook(TopOfBook inActualTopOfBook, AskEvent inExpectedAsk, BidEvent inExpectedBid) throws Exception { assertEquals(OrderBookTest.convertEvent(inExpectedBid), OrderBookTest.convertEvent(inActualTopOfBook.getBid())); assertEquals(OrderBookTest.convertEvent(inExpectedAsk), OrderBookTest.convertEvent(inActualTopOfBook.getAsk())); } /** * Verifies that the given exchange has the given expected attributes. * * @param inActualExchange a <code>SimulatedExchange</code> value * @param inExpectedName a <code>String</code> value * @param inExpectedCode a <code>String</code> value * @throws Exception if an error occurs */ private void verifyExchange(SimulatedExchange inActualExchange, String inExpectedName, String inExpectedCode) throws Exception { assertEquals(inExpectedName, inActualExchange.getName()); assertEquals(inExpectedCode, inActualExchange.getCode()); } /** * Verifies that the given <code>AggregateEvent</code> decomposes into the * given expected events. * * <p>No guarantee is made as to the order of the events. * * @param inActualEvent an <code>AggregateEvent</code> value * @param inExpectedEvents a <code>List<Event></code> value * @throws Exception if an error occurs */ final static void verifyDecomposedEvents(AggregateEvent inActualEvent, List<Event> inExpectedEvents) throws Exception { CollectionAssert.assertArrayPermutation(inExpectedEvents.toArray(), inActualEvent.decompose().toArray()); } /** * Verifies the given actual <code>DepthOfBook</code> contains the expected values. * * @param inActualDepthOfBook a <code>List<QuoteEvent></code> value * @param inExpectedAsks a <code>List<AskEvent></code> value * @param inExpectedBids a <code>List<BidEvent></code> value * @throws Exception if an error occurs */ public static void verifyDepthOfBook(List<QuoteEvent> inActualDepthOfBook, List<AskEvent> inExpectedAsks, List<BidEvent> inExpectedBids) throws Exception { List<BidEvent> actualBids = new ArrayList<BidEvent>(); List<AskEvent> actualAsks = new ArrayList<AskEvent>(); for(Event event : inActualDepthOfBook) { if(event instanceof BidEvent) { actualBids.add((BidEvent)event); } else if(event instanceof AskEvent) { actualAsks.add((AskEvent)event); } } assertEquals(OrderBookTest.convertEvents(inExpectedAsks), OrderBookTest.convertEvents(actualAsks)); assertEquals(OrderBookTest.convertEvents(inExpectedBids), OrderBookTest.convertEvents(actualBids)); } /** * Subscribes to top-of-book and captures the state of the exchange top every time it changes. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchangeTest.java 16395 2012-12-10 16:29:14Z colin $ * @since 1.5.0 */ @ThreadSafe private static class TopOfBookSubscriber implements ISubscriber { /* (non-Javadoc) * @see org.marketcetera.core.publisher.ISubscriber#isInteresting(java.lang.Object) */ @Override public boolean isInteresting(Object inData) { return inData instanceof QuoteEvent; } /* (non-Javadoc) * @see org.marketcetera.core.publisher.ISubscriber#publishTo(java.lang.Object) */ @Override public synchronized void publishTo(Object inData) { QuoteEvent quote = (QuoteEvent)inData; BidEvent lastBid = lastBids.get(quote.getInstrument()); AskEvent lastAsk = lastAsks.get(quote.getInstrument()); BidEvent newBid = (quote instanceof BidEvent ? quote.getAction() == QuoteAction.DELETE ? null : (BidEvent)quote : lastBid); AskEvent newAsk = (quote instanceof AskEvent ? quote.getAction() == QuoteAction.DELETE ? null : (AskEvent)quote : lastAsk); tops.add(new TopOfBook(newBid, newAsk)); lastBids.put(quote.getInstrument(), newBid); lastAsks.put(quote.getInstrument(), newAsk); } /** * * * * @return */ private List<TopOfBook> getTops() { return Collections.unmodifiableList(tops); } /** * the events received */ @GuardedBy("this") private final List<TopOfBook> tops = new ArrayList<TopOfBook>(); /** * the latest ask received, may be <code>null</code> */ @GuardedBy("this") private final Map<Instrument,AskEvent> lastAsks = new HashMap<Instrument,AskEvent>(); /** * the latest bid received, may be <code>null</code> */ @GuardedBy("this") private final Map<Instrument,BidEvent> lastBids = new HashMap<Instrument,BidEvent>(); } /** * Captures any events from a <code>SimulatedExchange</code>. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchangeTest.java 16395 2012-12-10 16:29:14Z colin $ * @since 1.5.0 */ private static class AllEventsSubscriber implements ISubscriber { /** * the events received */ private final List<Event> events = new ArrayList<Event>(); /* (non-Javadoc) * @see org.marketcetera.core.publisher.ISubscriber#isInteresting(java.lang.Object) */ @Override public boolean isInteresting(Object inData) { return true; } /* (non-Javadoc) * @see org.marketcetera.core.publisher.ISubscriber#publishTo(java.lang.Object) */ @Override public void publishTo(Object inData) { events.add((Event)inData); } } /** * Describes the expected state of a given <code>Instrument</code> in an unspecified exchange * at an unspecified point in time. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchangeTest.java 16395 2012-12-10 16:29:14Z colin $ * @since 2.0.0 */ private static class InstrumentState { /** * Create a new InstrumentState instance. * * @param inBids a <code>List<BidEvent></code> value * @param inAsks a <code>List<AskEvent></code> value * @param inTrade a <code>TradeEvent</code> value */ private InstrumentState(List<BidEvent> inBids, List<AskEvent> inAsks, TradeEvent inTrade) { bids = inBids; asks = inAsks; latestTrade = inTrade; } /** * the expected bids, may be empty */ private final List<BidEvent> bids; /** * the expected asks, may be empty */ private final List<AskEvent> asks; /** * the expected trade, may be <code>null</code> */ private final TradeEvent latestTrade; } /** * Organizes actual results from an exchange by <code>Instrument</code>. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchangeTest.java 16395 2012-12-10 16:29:14Z colin $ * @since 2.0.0 */ private static class EventOrganizer { /** * Process the given events to make them available by <code>Instrument</code> and purpose. * * @param inEvents a <code>List<? extends Event></code> value * @param inRequestType a <code>RequestType</code> value */ private static void process(List<? extends Event> inEvents, RequestType inRequestType) { Multimap<EventOrganizer,Event> sortedEvents = LinkedHashMultimap.create(); for(Event event : inEvents) { if(event instanceof HasInstrument) { Instrument instrument = ((HasInstrument)event).getInstrument(); EventOrganizer organizer = organizers.get(instrument); if(organizer == null) { organizer = new EventOrganizer(); organizers.put(instrument, organizer); } sortedEvents.put(organizer, event); } } for(EventOrganizer organizer : sortedEvents.keySet()) { Collection<Event> events = sortedEvents.get(organizer); switch(inRequestType) { case LATEST_TICK : organizer.latestTrade = null; if(events.size() > 1) { fail("Unable to translate " + events + " as latest tick (should be one event)"); } if(!events.isEmpty()) { organizer.latestTrade = (TradeEvent)events.iterator().next(); } break; case MARKET_STAT : organizer.marketstat = null; if(events.size() > 1) { fail("Unable to translate " + events + " as marketstat (should be one event)"); } if(!events.isEmpty()) { organizer.marketstat = (MarketstatEvent)events.iterator().next(); } break; case TOP_OF_BOOK : organizer.topOfBook.clear(); for(Event event : events) { if(event instanceof QuoteEvent) { organizer.topOfBook.add((QuoteEvent)event); } } break; case DEPTH_OF_BOOK : organizer.depthOfBook.clear(); for(Event event : events) { if(event instanceof QuoteEvent) { organizer.depthOfBook.add((QuoteEvent)event); } } break; default : fail("Unexpected request type"); } } } /** * Gets the <code>TopOfBook</code> that represents the current state of {@link #topOfBook}. * * @return a <code>TopOfBook</code> value */ private TopOfBook getTop() { assertTrue("There are too many events in the top-of-book collection", topOfBook.size() <= 2); BidEvent bid = null; AskEvent ask = null; while(!topOfBook.isEmpty()) { Event event = topOfBook.remove(0); if(event instanceof BidEvent) { bid = (BidEvent)event; } else if(event instanceof AskEvent) { ask = (AskEvent)event; } } return new TopOfBook(bid, ask); } /** * the set of event organizers by instrument */ private static Map<Instrument,EventOrganizer> organizers = new HashMap<Instrument,EventOrganizer>(); /** * the current depth-of-book for this instrument */ private final List<QuoteEvent> depthOfBook = new ArrayList<QuoteEvent>(); /** * the current top-of-book for this instrument */ private final List<QuoteEvent> topOfBook = new ArrayList<QuoteEvent>(); /** * the current lates trade for this instrument */ private TradeEvent latestTrade = null; /** * the current marketstat for this instrument */ private MarketstatEvent marketstat; /** * Indicates the type of request made to the exchange. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: SimulatedExchangeTest.java 16395 2012-12-10 16:29:14Z colin $ * @since 2.0.0 */ private static enum RequestType { LATEST_TICK, TOP_OF_BOOK, MARKET_STAT, DEPTH_OF_BOOK } } }