diff --git a/build.gradle b/build.gradle index 2613551..4fec85e 100644 --- a/build.gradle +++ b/build.gradle @@ -37,8 +37,6 @@ configure(allprojects) { exclude group: "org.apache.commons", name: "commons-lang" exclude group: "com.epam.deltix", name: "containers" exclude group: "com.epam.deltix", name: "hd-date-time" - exclude group: "com.epam.deltix", name: "gflog-api" - exclude group: "com.epam.deltix", name: "gflog-core" exclude group: "com.epam.deltix", name: "dfp" } } diff --git a/gradle.properties b/gradle.properties index b554a3f..cc3f9e6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=1.0.17-SNAPSHOT +version=1.0.18-SNAPSHOT group=com.epam.deltix org.gradle.jvmargs=-Xmx2500m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/EntryValidationCode.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/EntryValidationCode.java new file mode 100644 index 0000000..fcfdc59 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/EntryValidationCode.java @@ -0,0 +1,70 @@ +package com.epam.deltix.orderbook.core.api; + +/** + * Represents validation error codes that can be encountered during order book updates. + * Each constant corresponds to a specific scenario where an update to the order book fails + * validation checks. + */ +public enum EntryValidationCode { + /** + * The quote ID, which uniquely identifies an order, is missing. + */ + MISSING_QUOTE_ID, + + /** + * The quote ID submitted already exists in the order book, indicating a duplicated order. + */ + DUPLICATE_QUOTE_ID, + + /** + * The price field of the order is missing, which is required for order placement. + */ + MISSING_PRICE, + + /** + * The submitted order size is either not well-formed or outside the allowed range. + */ + BAD_SIZE, + + /** + * The quote ID referenced does not match any existing order in the order book. + */ + UNKNOWN_QUOTE_ID, + + /** + * An attempt to modify an existing order with a new price was made, + * which may not be supported in certain order book implementations. + */ + MODIFY_CHANGE_PRICE, + + /** + * An attempt to increase the size of an existing order through modification was made. + * This may be an invalid operation depending on exchange rules. + */ + MODIFY_INCREASE_SIZE, + + /** + * The exchange ID, used to identify the marketplace where the order should be placed, is missing. + */ + MISSING_EXCHANGE_ID, + + /** + * There's a discrepancy between the exchange ID stated and the one expected by the order book. + */ + EXCHANGE_ID_MISMATCH, + + /** + * The action specified for updating the order book is not recognized or allowed. + */ + UNSUPPORTED_UPDATE_ACTION, + + /** + * The order does not specify whether it is a buy or sell order. + */ + UNSPECIFIED_SIDE, + + /** + * The type of order insert operation specified is not supported by the order book. + */ + UNSUPPORTED_INSERT_TYPE +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/ErrorListener.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/ErrorListener.java new file mode 100644 index 0000000..924848a --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/ErrorListener.java @@ -0,0 +1,17 @@ +package com.epam.deltix.orderbook.core.api; + + +import com.epam.deltix.timebase.messages.MessageInfo; + +/** + * User-defined error handler + */ +public interface ErrorListener { + /** + * Called when input market message contains something invalid. + * + * @param message message containing invalid market-related messages + * @param errorCode error code that describes what is wrong + */ + void onError(MessageInfo message, EntryValidationCode errorCode); +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/Exchange.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/Exchange.java index 5f5ef3c..6cfa438 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/Exchange.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/Exchange.java @@ -16,8 +16,8 @@ */ package com.epam.deltix.orderbook.core.api; - import com.epam.deltix.timebase.messages.universal.QuoteSide; +import com.epam.deltix.util.annotations.Alphanumeric; /** * Represents the order book entries for a specific stock exchange. @@ -32,6 +32,7 @@ public interface Exchange { * * @return exchangeId for this exchange. */ + @Alphanumeric long getExchangeId(); /** diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/ExchangeList.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/ExchangeList.java index 3ed894a..028a68e 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/ExchangeList.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/ExchangeList.java @@ -17,6 +17,7 @@ package com.epam.deltix.orderbook.core.api; import com.epam.deltix.orderbook.core.options.Option; +import com.epam.deltix.util.annotations.Alphanumeric; import java.util.function.BiPredicate; import java.util.function.Predicate; @@ -30,7 +31,7 @@ * * @author Andrii_Ostapenko1 */ -public interface ExchangeList extends Iterable { +public interface ExchangeList extends Iterable { /** * Get exchange holder by id. @@ -40,7 +41,7 @@ public interface ExchangeList extends Iterable { * @return an {@code Optional} containing the exchange holder; never {@code null} but * potentially empty */ - Option getById(long exchangeId); + Option getById(@Alphanumeric long exchangeId); /** * Returns the number of elements in this list. @@ -69,7 +70,7 @@ public interface ExchangeList extends Iterable { * @see Stream * @see java.util.Spliterator */ - default Stream stream(final boolean parallel) { + default Stream stream(final boolean parallel) { return StreamSupport.stream(spliterator(), parallel); } @@ -83,21 +84,21 @@ default Stream stream(final boolean parallel) { * @see Stream * @see java.util.Spliterator */ - default Stream stream() { + default Stream stream() { return stream(false); } - default void forEach(final Predicate action) { - for (final StockExchange e : this) { + default void forEach(final Predicate action) { + for (final Exchange e : this) { if (!action.test(e)) { return; } } } - default void forEach(final BiPredicate action, + default void forEach(final BiPredicate action, final Cookie cookie) { - for (final StockExchange e : this) { + for (final Exchange e : this) { if (!action.test(e, cookie)) { return; } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/IterableMarketSide.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/IterableMarketSide.java index 8a0b39a..e7e6448 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/IterableMarketSide.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/IterableMarketSide.java @@ -37,6 +37,7 @@ public interface IterableMarketSide extends Iterable { * * @return an iterator over the elements in this market side in proper sequence */ + @Override Iterator iterator(); /** @@ -45,7 +46,7 @@ public interface IterableMarketSide extends Iterable { * @param fromLevel - Starting price level index to use * @return an iterator over the elements in this order book in proper sequence */ - Iterator iterator(short fromLevel); + Iterator iterator(int fromLevel); /** * Returns an iterator over the elements in this market side in proper sequence. @@ -54,7 +55,7 @@ public interface IterableMarketSide extends Iterable { * @param toLevel - End price level index to use * @return an iterator over the elements in this order book in proper sequence */ - Iterator iterator(short fromLevel, short toLevel); + Iterator iterator(int fromLevel, int toLevel); /** * Creates a new sequential or parallel {@code Stream} from a @@ -89,13 +90,13 @@ default Stream stream() { void forEach(Predicate action); - void forEach(short fromLevel, Predicate action); + void forEach(int fromLevel, Predicate action); - void forEach(short fromLevel, short toLevel, Predicate action); + void forEach(int fromLevel, int toLevel, Predicate action); void forEach(BiPredicate action, Cookie cookie); - void forEach(short fromLevel, BiPredicate action, Cookie cookie); + void forEach(int fromLevel, BiPredicate action, Cookie cookie); - void forEach(short fromLevel, short toLevel, BiPredicate action, Cookie cookie); + void forEach(int fromLevel, int toLevel, BiPredicate action, Cookie cookie); } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/MarketSide.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/MarketSide.java index 605cca0..d23c8af 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/MarketSide.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/MarketSide.java @@ -49,7 +49,14 @@ public interface MarketSide extends IterableMarketSide { Quote getBestQuote(); /** - * Get quote by level. + * Get worst quote. + * + * @return Worst quote from side or null if quote not found + */ + Quote getWorstQuote(); + + /** + * Get quote by level. WARNING: this method can be slow for some implementations (for example, in L3 order book). Use iterator instead. * * @param level - level to use * @return quote or null if quote not found @@ -89,30 +96,47 @@ public interface MarketSide extends IterableMarketSide { * @param level - quote level to use. * @return true if this market side contains given quote level */ - boolean hasLevel(short level); + boolean hasLevel(int level); + + /** + * Get quote by quoteId. + * Unsupported operation for L2, always returns null. + * + * @param quoteId - Quote Id + * @return quote or null if quote not found + */ + Quote getQuote(CharSequence quoteId); + + /** + * Unsupported operation for L2, always returns false. + * + * @param quoteId - Quote Id + * @return true if this market side contains given quote ID + */ + boolean hasQuote(CharSequence quoteId); @Override default Iterator iterator() { - return iterator((short) 0); + return iterator(0); } @Override - default Iterator iterator(final short fromLevel) { - return iterator(fromLevel, (short) depth()); + default Iterator iterator(final int fromLevel) { + return iterator(fromLevel, depth()); } @Override default void forEach(final Predicate action) { - forEach((short) 0, (short) depth(), action); + forEach(0, depth(), action); } @Override - default void forEach(final short level, final Predicate action) { - forEach((short) 0, level, action); + default void forEach(final int level, final Predicate action) { + forEach(0, level, action); } @Override - default void forEach(final short fromLevel, final short toLevel, final Predicate action) { + default void forEach(final int fromLevel, final int toLevel, final Predicate action) { Objects.requireNonNull(action); for (int i = fromLevel; i < toLevel; i++) { if (!action.test(getQuote(i))) { @@ -123,17 +147,17 @@ default void forEach(final short fromLevel, final short toLevel, final Predicate @Override default void forEach(final BiPredicate action, final Cookie cookie) { - forEach((short) 0, (short) depth(), action, cookie); + forEach(0, depth(), action, cookie); } @Override - default void forEach(final short fromLevel, final BiPredicate action, final Cookie cookie) { - forEach(fromLevel, (short) depth(), action, cookie); + default void forEach(final int fromLevel, final BiPredicate action, final Cookie cookie) { + forEach(fromLevel, depth(), action, cookie); } @Override - default void forEach(final short fromLevel, - final short toLevel, + default void forEach(final int fromLevel, + final int toLevel, final BiPredicate action, final Cookie cookie) { Objects.requireNonNull(action); diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBook.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBook.java index 1baca11..a8f121a 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBook.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBook.java @@ -16,8 +16,9 @@ */ package com.epam.deltix.orderbook.core.api; + import com.epam.deltix.orderbook.core.options.Option; -import com.epam.deltix.timebase.messages.MarketMessageInfo; +import com.epam.deltix.timebase.messages.MessageInfo; import com.epam.deltix.timebase.messages.universal.DataModelType; import com.epam.deltix.timebase.messages.universal.QuoteSide; @@ -28,10 +29,10 @@ * of various trading systems that we have previously worked with. It is designed to provide high-level normalized * format suitable to capture data from majority of very different trading venues. * Package Type is enumeration of: - * •VENDOR_SNAPSHOT – snapshot that came directly from data vendor (e.g. exchange). - * •PERIODICAL_SNAPSHOT Synthetic snapshot generated by EPAM Market + * VENDOR_SNAPSHOT - snapshot that came directly from data vendor (e.g. exchange). + * PERIODICAL_SNAPSHOT Synthetic snapshot generated by EPAM Market * Data Aggregation software to simplify data validation and allow late joiners (consumers that appear in the middle of trading session). - * •INCREMENTAL_UPDATE – incremental update, a list of insert/update/delete data entries. + * INCREMENTAL_UPDATE - incremental update, a list of insert/update/delete data entries. * This Format does not support snapshot and increment messages mixed in one package. * Trade entries can be easily combined with Increments. * It is important to differentiate one type of snapshot from another. @@ -52,7 +53,7 @@ public interface OrderBook { * Note: FlyWeight pattern in use. We don't keep any references on your classes (message) after method returns execution. *

*

- * Supported market data messages: L1/L2/ResetEntry + * Supported market data messages: L1/L2/L3/ResetEntry, * * @param message Most financial market-related messages to use. * @return {@code true} if all entries of the package is process otherwise {@code false} @@ -63,7 +64,7 @@ public interface OrderBook { * @see com.epam.deltix.timebase.messages.universal.BookResetEntryInterface *

*/ - boolean update(MarketMessageInfo message); + boolean update(MessageInfo message); /** * Returns true if this order book contains no quotes, applies to both sides of Order Book. @@ -119,4 +120,11 @@ public interface OrderBook { */ ExchangeList> getExchanges(); + /** + * @return true if order book from this exchange is waiting for snapshot to recover. + * In this state order book appears empty, but corresponding exchange is likely not empty. + * Order book may be in this state initially, or after we market data disconnect, as well as after internal error. + */ + boolean isWaitingForSnapshot(); + } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBookFactory.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBookFactory.java index c9433bd..06c04ff 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBookFactory.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBookFactory.java @@ -16,9 +16,14 @@ */ package com.epam.deltix.orderbook.core.api; + import com.epam.deltix.orderbook.core.impl.L1OrderBookFactory; import com.epam.deltix.orderbook.core.impl.L2OrderBookFactory; -import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.orderbook.core.impl.L3OrderBookFactory; +import com.epam.deltix.orderbook.core.options.Defaults; +import com.epam.deltix.orderbook.core.options.OrderBookOptions; +import com.epam.deltix.orderbook.core.options.OrderBookOptionsBuilder; +import com.epam.deltix.orderbook.core.options.OrderBookType; import com.epam.deltix.timebase.messages.universal.DataModelType; import java.util.Objects; @@ -42,52 +47,59 @@ protected OrderBookFactory() { /** * Factory method for create order book with the given options. * - *

- * Note: FlyWeight pattern in use. We don't keep any references on your classes (opt) after method returns execution. - * * @param type of quote - * @param opt to use. + * @param options to use. * @return a new OrderBook instance of given type. - * @throws NullPointerException - if opt is null. - * @throws UnsupportedOperationException - if some options does not supported. + * @throws IllegalArgumentException - if some options does not supported. * @see OrderBook * @see OrderBookQuote */ - public static OrderBook create(final OrderBookOptions opt) { - Objects.requireNonNull(opt); - final Option symbol = opt.getSymbol(); - final DataModelType quoteLevels = opt.getQuoteLevels().orElse(Defaults.QUOTE_LEVELS); - final OrderBookType orderBookType = opt.getBookType().orElse(Defaults.ORDER_BOOK_TYPE); - OrderBook book = null; - final UpdateMode updateMode = opt.getUpdateMode().orElse(Defaults.UPDATE_MODE); + public static OrderBook create(final OrderBookOptions options) { + if (Objects.isNull(options)) { + throw new IllegalArgumentException("Options not allowed to be null."); + } + + final DataModelType quoteLevels = options.getQuoteLevels().orElse(Defaults.QUOTE_LEVELS); + final OrderBookType orderBookType = options.getBookType().orElse(Defaults.ORDER_BOOK_TYPE); + final OrderBook book; switch (quoteLevels) { case LEVEL_ONE: if (orderBookType == OrderBookType.SINGLE_EXCHANGE) { - book = L1OrderBookFactory.newSingleExchangeBook(symbol, updateMode); + book = L1OrderBookFactory.newSingleExchangeBook(options); } else { - throw new UnsupportedOperationException("Unsupported book mode: " + orderBookType + " for quote levels: " + quoteLevels); + throw new IllegalArgumentException("Unsupported book type: " + orderBookType + " for quote levels: " + quoteLevels); } break; case LEVEL_TWO: - final GapMode gapMode = opt.getGapMode().orElse(Defaults.GAP_MODE); - final UnreachableDepthMode unreachableDepthMode = opt.getUnreachableDepthMode().orElse(Defaults.UNREACHABLE_DEPTH_MODE); - final int initialDepth = opt.getInitialDepth().orElse(Defaults.INITIAL_DEPTH); - final int maxDepth = opt.getMaxDepth().orElse(Defaults.MAX_DEPTH); - final Integer exchangePoolSize = opt.getInitialExchangesPoolSize().orElse(Defaults.INITIAL_EXCHANGES_POOL_SIZE); switch (orderBookType) { case SINGLE_EXCHANGE: - book = L2OrderBookFactory.newSingleExchangeBook(symbol, initialDepth, maxDepth, gapMode, updateMode, unreachableDepthMode); + book = L2OrderBookFactory.newSingleExchangeBook(options); break; case AGGREGATED: - book = L2OrderBookFactory.newAggregatedBook(symbol, exchangePoolSize, initialDepth, maxDepth, gapMode, updateMode, unreachableDepthMode); + book = L2OrderBookFactory.newAggregatedBook(options); + break; + case CONSOLIDATED: + book = L2OrderBookFactory.newConsolidatedBook(options); + break; + default: + throw new IllegalArgumentException("Unsupported book type: " + orderBookType + " for quote levels: " + quoteLevels); + } + break; + case LEVEL_THREE: + switch (orderBookType) { + case SINGLE_EXCHANGE: + book = L3OrderBookFactory.newSingleExchangeBook(options); break; case CONSOLIDATED: - book = L2OrderBookFactory.newConsolidatedBook(symbol, exchangePoolSize, initialDepth, maxDepth, gapMode, updateMode, unreachableDepthMode); + book = L3OrderBookFactory.newConsolidatedBook(options); break; + case AGGREGATED: + default: + throw new IllegalArgumentException("Unsupported book type: " + orderBookType + " for quote levels: " + quoteLevels); } break; default: - throw new UnsupportedOperationException("Unsupported quote levels: " + quoteLevels); + throw new IllegalArgumentException("Unsupported quote levels: " + quoteLevels); } return book; } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBookQuote.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBookQuote.java index c1068ca..dd23622 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBookQuote.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBookQuote.java @@ -23,7 +23,7 @@ * * @author Andrii_Ostapenko1 */ -public interface OrderBookQuote { +public interface OrderBookQuote extends OrderBookQuoteTimestamp { /** * Ask, Bid or Trade price. @@ -83,4 +83,44 @@ public interface OrderBookQuote { */ boolean hasSize(); + /** + * Quote ID. In Forex market, for example, quote ID can be referenced in + * TradeOrders (to identify market maker's quote/rate we want to deal with). + * Each market maker usually keeps this ID unique per session per day. This + * is a alpha-numeric text field that can reach 64 characters or more, + *

+ * Supported for L3 quote level + * + * @return Quote ID or null if not found + */ + CharSequence getQuoteId(); + + /** + * Quote ID. In Forex market, for example, quote ID can be referenced in + * TradeOrders (to identify market maker's quote/rate we want to deal with). + * Each market maker usually keeps this ID unique per session per day. This + * is a alpha-numeric text field that can reach 64 characters or more, + *

+ * Supported for L3 quote level + * + * @return true if Quote ID is not null + */ + boolean hasQuoteId(); + + /** + * Id of participant (or broker ID). + * Supported for L3 quote level + * + * @return Participant or null if not found + */ + CharSequence getParticipantId(); + + /** + * Id of participant (or broker ID). + * Supported for L3 quote level + * + * @return true if Participant is not null + */ + boolean hasParticipantId(); + } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBookQuoteTimestamp.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBookQuoteTimestamp.java new file mode 100644 index 0000000..65cdb6e --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/api/OrderBookQuoteTimestamp.java @@ -0,0 +1,73 @@ +package com.epam.deltix.orderbook.core.api; + +/** + * Quote timestamp interface. + *

+ * This interface is used for accessing quote timestamp information. + * This functionality is optional and can be enabled by setting OrderBookOptionsBuilder.shouldStoreQuoteTimestamps(true) + *

+ * This function requires additional memory allocation for each quote. + * + * @author Andrii_Ostapenko1 + * @see com.epam.deltix.orderbook.core.options.Defaults#SHOULD_STORE_QUOTE_TIMESTAMPS + */ +public interface OrderBookQuoteTimestamp { + + /** + * Special constant that marks 'unknown' timestamp. + */ + long TIMESTAMP_UNKNOWN = Long.MIN_VALUE; + + /** + * Exchange Time is measured in milliseconds that passed since January 1, 1970 UTC + * For inbound messages special constant {link TIMESTAMP_UNKNOWN} marks 'unknown' timestamp in which case OrderBook + * stores message using current server time. + *

+ * By default, Original Timestamp is not supported. + * For enabling Original Timestamp support you need to set OrderBookOptionsBuilder.shouldStoreQuoteTimestamps(true) + * + * @return timestamp + */ + default long getOriginalTimestamp() { + return TIMESTAMP_UNKNOWN; + } + + /** + * Exchange Time is measured in milliseconds that passed since January 1, 1970 UTC + * By default Original Timestamp is not supported. + * For enabling Original Timestamp support you need to set OrderBookOptionsBuilder.shouldStoreQuoteTimestamps(true) + * + * @return true if Original Timestamp is not null + */ + default boolean hasOriginalTimestamp() { + return false; + } + + /** + * Time in this field is measured in milliseconds that passed since January 1, 1970 UTC. + * For inbound messages, special constant {link TIMESTAMP_UNKNOWN} marks 'unknown' timestamp + * in which case OrderBook stores message using current server time. + *

+ * By default, TimeStamp is not supported. + * For enabling Time support you need to set OrderBookOptionsBuilder.shouldStoreQuoteTimestamps(true) + * + * @return timestamp + */ + default long getTimestamp() { + return TIMESTAMP_UNKNOWN; + } + + /** + * Time in this field is measured in milliseconds that passed since January 1, 1970 UTC. + * For inbound messages, special constant {link TIMESTAMP_UNKNOWN} marks 'unknown' timestamp + * in which case OrderBook stores message using current server time. + *

+ * By default, TimeStamp is not supported. + * For enabling Time support, you need to set OrderBookOptionsBuilder.shouldStoreQuoteTimestamps(true) + * + * @return true if time not null + */ + default boolean hasTimestamp() { + return false; + } +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL1MarketSide.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL1MarketSide.java index 2150290..71ce2a8 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL1MarketSide.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL1MarketSide.java @@ -69,7 +69,7 @@ public boolean isEmpty() { } @Override - public boolean hasLevel(final short level) { + public boolean hasLevel(final int level) { return !isEmpty() && level == 0; } @@ -78,6 +78,11 @@ public Quote getBestQuote() { return quote; } + @Override + public Quote getWorstQuote() { + return quote; + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); @@ -88,7 +93,7 @@ public String toString() { } @Override - public Iterator iterator(final short fromLevel, final short toLevel) { + public Iterator iterator(final int fromLevel, final int toLevel) { itr.iterateBy(quote); return itr; } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL2MarketSide.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL2MarketSide.java index 218cf44..38e1834 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL2MarketSide.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL2MarketSide.java @@ -19,13 +19,18 @@ import com.epam.deltix.dfp.Decimal; import com.epam.deltix.dfp.Decimal64Utils; import com.epam.deltix.orderbook.core.api.MarketSide; +import com.epam.deltix.timebase.messages.universal.BookUpdateAction; import com.epam.deltix.timebase.messages.universal.QuoteSide; +import com.epam.deltix.util.annotations.Alphanumeric; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Objects; +import static com.epam.deltix.dfp.Decimal64Utils.*; +import static com.epam.deltix.timebase.messages.TypeConstants.EXCHANGE_NULL; + /** * @author Andrii_Ostapenko1 */ @@ -33,21 +38,17 @@ abstract class AbstractL2MarketSide impleme protected final List data; private final ReusableIterator itr; - // This parameter is used to limit maximum elements. - private final short maxDepth; - // This parameter is used to understand whether the side is full or not. - private short depthLimit; + // This parameter is used to limit maximum elements and to understand whether the side is full or not. + private final int maxDepth; - AbstractL2MarketSide(final int initialCapacity, - final short maxDepth) { + AbstractL2MarketSide(final int initialCapacity, final int maxDepth) { this.maxDepth = maxDepth; - this.depthLimit = maxDepth; this.data = new ArrayList<>(initialCapacity); this.itr = new ReusableIterator<>(); } @Override - public short getMaxDepth() { + public int getMaxDepth() { return maxDepth; } @@ -59,8 +60,7 @@ public int depth() { //TODO Add configuration parameter for type of calculating total quantity @Override public long getTotalQuantity() { - @Decimal - long result = Decimal64Utils.ZERO; + @Decimal long result = ZERO; for (int i = 0; i < data.size(); i++) { result = Decimal64Utils.add(result, data.get(i).getSize()); } @@ -79,37 +79,32 @@ public boolean isEmpty() { @Override public Quote getQuote(final int level) { - if (!hasLevel((short) level)) { + if (!hasLevel(level)) { return null; } return data.get(level); } @Override - public void add(final short level, final Quote insert) { + public void add(final int level, final Quote insert) { data.add(level, insert); } @Override - public void addLast(final Quote insert) { - data.add(insert); - } - - @Override - public void add(final Quote insert) { + public void addWorstQuote(final Quote insert) { data.add(insert); } @Override public Quote remove(final int level) { - if (!hasLevel((short) level)) { + if (!hasLevel(level)) { return null; } return data.remove(level); } @Override - public short binarySearchLevelByPrice(final Quote find) { + public int binarySearch(final Quote find) { int low = 0; int high = data.size() - 1; @@ -149,14 +144,14 @@ public short binarySearchLevelByPrice(final Quote find) { high = mid - 1; } } else { - return (short) mid; + return mid; } } return NOT_FOUND; } @Override - public short binarySearchNextLevelByPrice(final Quote find) { + public int binarySearchNextLevelByPrice(final Quote find) { int low = 0; int high = data.size() - 1; @@ -196,30 +191,20 @@ public short binarySearchNextLevelByPrice(final Quote find) { high = mid - 1; } } else { - return (short) mid; + return mid; } } - return (short) low; + return low; } @Override - public boolean hasLevel(final short level) { - if (data.size() > level) { + public boolean hasLevel(final int level) { + if (level >= 0 && data.size() > level) { return Objects.nonNull(data.get(level)); } return false; } - @Override - public void trim() { - this.depthLimit = (short) data.size(); - } - - @Override - public Quote getWorstQuote() { - return data.get(data.size() - 1); - } - @Override public Quote removeWorstQuote() { return data.remove(data.size() - 1); @@ -227,15 +212,20 @@ public Quote removeWorstQuote() { @Override public boolean isFull() { - return depth() >= depthLimit; + return depth() >= maxDepth; } //TODO add doc !! @Override - public boolean isGap(final short level) { + public boolean isGap(final int level) { return !hasLevel(level) && level > depth(); } + @Override + public boolean isUnreachableLeve(int level) { + return level < 0 || level >= getMaxDepth(); + } + @Override public Quote getBestQuote() { if (isEmpty()) { @@ -244,6 +234,90 @@ public Quote getBestQuote() { return data.get(0); } + @Override + public Quote getWorstQuote() { + if (!isEmpty()) { + return data.get(data.size() - 1); + } + return null; + } + + @Override + public boolean isInvalidInsert(final int level, final @Decimal long price, final @Decimal long size, final @Alphanumeric long exchangeId) { + //TODO need to defined default type for internal decimal + if (level < 0 || isEqual(price, NULL) || isLessOrEqual(size, ZERO) || exchangeId == EXCHANGE_NULL) { + return true; + } + if (isUnreachableLeve(level)) { + return true; + } + if (isGap(level)) { + return true; + } + return !checkOrderPrice(level, price); + } + + @Override + public boolean isInvalidUpdate(final BookUpdateAction action, + final int level, + final @Decimal long price, + final @Decimal long size, + final @Alphanumeric long exchangeId) { + if (!hasLevel(level)) { + return true; + } + if (action != BookUpdateAction.DELETE) { + return isNotEqual(getQuote(level).getPrice(), price) || isLess(size, ZERO); + } + return false; + } + + /** + * Checking the insertion of the quotation price. + * @param level - quote level to use + * @param price - price to be checked + * @return true if this price is sorted. + */ + @Override + public boolean checkOrderPrice(final int level, final @Decimal long price) { + + @Decimal final long previousPrice = hasLevel(level - 1) ? getQuote(level - 1).getPrice() : NULL; + @Decimal final long nextPrice = hasLevel(level) ? getQuote(level).getPrice() : NULL; + + boolean badState = false; + if (getSide() == QuoteSide.ASK) { + if (isNotEqual(previousPrice, NULL) && isGreater(previousPrice, price)) { + badState = true; + } + if (isNotEqual(nextPrice, NULL) && isLess(nextPrice, price)) { + badState = true; + } + } else { + if (isNotEqual(previousPrice, NULL) && isLess(previousPrice, price)) { + badState = true; + } + if (isNotEqual(nextPrice, NULL) && isGreater(nextPrice, price)) { + badState = true; + } + } + return !badState; + } + + @Override + public boolean validateState() { + if (isEmpty()) { + return true; + } + for (int i = 0; i < depth(); i++) { + final Quote quote = getQuote(i); + if (isInvalidInsert(i, quote.getPrice(), quote.getSize(), quote.getExchangeId())) { + //TODO add log + return false; + } + } + return true; + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); @@ -254,7 +328,7 @@ public String toString() { } @Override - public Iterator iterator(final short fromLevel, final short toLevel) { + public Iterator iterator(final int fromLevel, final int toLevel) { itr.iterateBy(this, fromLevel, toLevel); return itr; } @@ -267,18 +341,18 @@ static final class ReusableIterator implements Iterator { /** * Index of element to be returned by subsequent call to next. */ - private short cursor; + private int cursor; - private short size; + private int size; private MarketSide marketSide; - private void iterateBy(final MarketSide marketSide, final short cursor, final short size) { + private void iterateBy(final MarketSide marketSide, final int cursor, final int size) { Objects.requireNonNull(marketSide); this.marketSide = marketSide; this.cursor = cursor; if (size > marketSide.depth() || size < 0) { - this.size = (short) marketSide.depth(); + this.size = marketSide.depth(); } else { this.size = size; } @@ -305,7 +379,7 @@ public void remove() { static class ASK extends AbstractL2MarketSide { - ASK(final int initialCapacity, final short maxDepth) { + ASK(final int initialCapacity, final int maxDepth) { super(initialCapacity, maxDepth); } @@ -318,7 +392,7 @@ public QuoteSide getSide() { static class BID extends AbstractL2MarketSide { - BID(final int initialDepth, final short maxDepth) { + BID(final int initialDepth, final int maxDepth) { super(initialDepth, maxDepth); } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL2MultiExchangeProcessor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL2MultiExchangeProcessor.java index 31d2e6e..0db2d3b 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL2MultiExchangeProcessor.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL2MultiExchangeProcessor.java @@ -16,12 +16,17 @@ */ package com.epam.deltix.orderbook.core.impl; +import com.epam.deltix.containers.AlphanumericUtils; import com.epam.deltix.orderbook.core.api.MarketSide; import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.timebase.messages.TypeConstants; +import com.epam.deltix.timebase.messages.service.FeedStatus; +import com.epam.deltix.timebase.messages.service.SecurityFeedStatusMessage; import com.epam.deltix.timebase.messages.universal.*; import com.epam.deltix.util.annotations.Alphanumeric; import com.epam.deltix.util.collections.generated.ObjectList; + /** * Main class for L2 quote level order book. * @@ -32,38 +37,46 @@ abstract class AbstractL2MultiExchangeProcessor bids; protected final L2MarketSide asks; + protected final ObjectPool pool; + protected final MutableExchangeList>> exchanges; //Parameters - protected final GapMode gapMode; - protected final UpdateMode updateMode; - protected final UnreachableDepthMode unreachableDepthMode; - protected final ObjectPool pool; - protected final short initialDepth; - protected final int maxDepth; - /** - * This parameter using for handle book reset entry. - * - * @see QuoteProcessor#isWaitingForSnapshot() - */ - private boolean isWaitingForSnapshot = false; - - AbstractL2MultiExchangeProcessor(final int initialExchangeCount, - final int initialDepth, - final int maxDepth, - final ObjectPool pool, - final GapMode gapMode, - final UpdateMode updateMode, - final UnreachableDepthMode unreachableDepthMode) { - this.initialDepth = (short) initialDepth; - this.maxDepth = maxDepth; + protected final DisconnectMode disconnectMode; + protected final ValidationOptions validationOptions; + private final OrderBookOptions options; + + AbstractL2MultiExchangeProcessor(final OrderBookOptions options, final ObjectPool pool) { + this.options = options; + this.validationOptions = options.getInvalidQuoteMode().orElse(Defaults.VALIDATION_OPTIONS); + this.disconnectMode = options.getDisconnectMode().orElse(Defaults.DISCONNECT_MODE); + + final int maxDepth = options.getMaxDepth().orElse(Defaults.MAX_DEPTH); + final int depth = options.getInitialDepth().orElse(Math.min(Defaults.INITIAL_DEPTH, maxDepth)); + final int exchanges = options.getInitialExchangesPoolSize().orElse(Defaults.INITIAL_EXCHANGES_POOL_SIZE); this.pool = pool; - this.gapMode = gapMode; - this.updateMode = updateMode; - this.unreachableDepthMode = unreachableDepthMode; - this.exchanges = new MutableExchangeListImpl<>(initialExchangeCount); - this.asks = L2MarketSide.factory(initialExchangeCount * initialDepth, Defaults.MAX_DEPTH, QuoteSide.ASK); - this.bids = L2MarketSide.factory(initialExchangeCount * initialDepth, Defaults.MAX_DEPTH, QuoteSide.BID); + this.exchanges = new MutableExchangeListImpl<>(exchanges); + this.asks = L2MarketSide.factory(exchanges * depth, Defaults.MAX_DEPTH, QuoteSide.ASK); + this.bids = L2MarketSide.factory(exchanges * depth, Defaults.MAX_DEPTH, QuoteSide.BID); + } + + @Override + public boolean isWaitingForSnapshot() { + if (exchanges.isEmpty()) { + return true; // No data from exchanges, so we are in "waiting" state + } + + for (final MutableExchange exchange : exchanges) { + if (exchange.isWaitingForSnapshot()) { + return true; // At least one of source exchanges awaits snapshot + } + } + return false; + } + + @Override + public boolean isSnapshotAllowed(final PackageHeaderInfo msg) { + throw new UnsupportedOperationException("Unsupported for multi exchange processor!"); } @Override @@ -82,154 +95,144 @@ public boolean isEmpty() { } @Override - public void processBookResetEntry(final BookResetEntryInfo bookResetEntryInfo) { - clearExchange(bookResetEntryInfo.getExchangeId()); - waitingForSnapshot(); + public boolean processSecurityFeedStatus(final SecurityFeedStatusMessage msg) { + if (msg.getStatus() == FeedStatus.NOT_AVAILABLE) { + if (disconnectMode == DisconnectMode.CLEAR_EXCHANGE) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final Option>> holder = getOrCreateExchange(exchangeId); + + if (!holder.hasValue()) { + return false; + } + final L2Processor exchange = holder.get().getProcessor(); + + unmapQuote(exchange); + return exchange.processSecurityFeedStatus(msg); + } + } + return false; } @Override - public void processL2VendorSnapshot(final PackageHeaderInfo marketMessageInfo) { - final ObjectList entries = marketMessageInfo.getEntries(); - @Alphanumeric final long exchangeId = entries.get(0).getExchangeId(); + public boolean processBookResetEntry(final PackageHeaderInfo pck, final BookResetEntryInfo msg) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final Option>> holder = getOrCreateExchange(exchangeId); - final L2Processor exchange = clearExchange(exchangeId); - - exchange.processL2VendorSnapshot(marketMessageInfo); + if (!holder.hasValue()) { + return false; + } + final L2Processor exchange = holder.get().getProcessor(); - insertAll(exchange, QuoteSide.BID); - insertAll(exchange, QuoteSide.ASK); - notWaitingForSnapshot(); + unmapQuote(exchange); + return exchange.processBookResetEntry(pck, msg); } @Override - public Quote processL2EntryNewInfo(final L2EntryNewInfo l2EntryNewInfo) { - final QuoteSide side = l2EntryNewInfo.getSide(); - final long exchangeId = l2EntryNewInfo.getExchangeId(); - final short level = l2EntryNewInfo.getLevel(); - final L2Processor exchange = getOrCreateExchange(exchangeId); + public boolean processL2Snapshot(final PackageHeaderInfo msg) { + final ObjectList entries = msg.getEntries(); - // Duplicate - if (exchange.isEmpty()) { - switch (updateMode) { - case WAITING_FOR_SNAPSHOT: - return null; // Todo ADD null check!! - case NON_WAITING_FOR_SNAPSHOT: - break; - default: - throw new UnsupportedOperationException("Unsupported mode: " + updateMode); + // we assume that all entries in the message are from the same exchange + @Alphanumeric final long exchangeId = entries.get(0).getExchangeId(); + + final Option>> holder = getOrCreateExchange(exchangeId); + + if (!holder.hasValue()) { + return false; + } + + final L2Processor exchange = holder.get().getProcessor(); + if (exchange.isSnapshotAllowed(msg)) { + unmapQuote(exchange); + if (exchange.processL2Snapshot(msg)) { + mapQuote(exchange, QuoteSide.BID); + mapQuote(exchange, QuoteSide.ASK); + return true; } } + return false; + } - final L2MarketSide marketSide = exchange.getMarketSide(side); + @Override + public Quote processL2EntryNew(final PackageHeaderInfo pck, final L2EntryNewInfo msg) { + assert pck.getPackageType() == PackageType.INCREMENTAL_UPDATE; + final QuoteSide side = msg.getSide(); + final int level = msg.getLevel(); + @Alphanumeric final long exchangeId = msg.getExchangeId(); - // TODO: 6/30/2022 need to refactor return value + final Option>> holder = getOrCreateExchange(exchangeId); // Duplicate - if (level >= marketSide.getMaxDepth()) { - switch (unreachableDepthMode) { - case SKIP_AND_DROP: - clear(); - return null; - case SKIP: - default: - return null; - } - // Unreachable quote level + if (!holder.hasValue() || holder.get().getProcessor().isWaitingForSnapshot()) { + return null; } - // TODO: 6/30/2022 need to refactor return value - // Duplicate - if (marketSide.isGap(level)) { - switch (gapMode) { - case FILL_GAP:// We fill gaps at the exchange level. - break; - case SKIP_AND_DROP: - clearExchange(exchange); - return null; - case SKIP: - return null; - default: - throw new UnsupportedOperationException("Unsupported mode: " + gapMode); + final L2Processor exchange = holder.get().getProcessor(); + + final L2MarketSide marketSide = exchange.getMarketSide(side); + if (marketSide.isInvalidInsert(level, msg.getPrice(), msg.getSize(), exchangeId)) { + if (validationOptions.isQuoteInsert()) { + unmapQuote(exchange); + exchange.processL2EntryNew(pck, msg); } + return null; } - if (marketSide.hasLevel(level)) { //Remove worst quote + //Remove worst quote + //...maybe we should remove + if (marketSide.isFull()) { removeQuote(marketSide.getWorstQuote(), side); } - final Quote quote = exchange.processL2EntryNewInfo(l2EntryNewInfo); + // We process quote as new by single exchange and then insert it to the aggregated book + final Quote quote = exchange.processL2EntryNew(pck, msg); + if (quote == null) { + return null; + } final Quote insertQuote = insertQuote(quote, side); - return insertQuote; } @Override - public void processL2EntryUpdateInfo(final L2EntryUpdateInfo l2EntryUpdateInfo) { - final long exchangeId = l2EntryUpdateInfo.getExchangeId(); - final L2Processor exchange = getOrCreateExchange(exchangeId); - - if (exchange.isEmpty()) { - return; + public boolean processL2EntryUpdate(final PackageHeaderInfo pck, final L2EntryUpdateInfo msg) { + assert pck.getPackageType() == PackageType.INCREMENTAL_UPDATE; + final int level = msg.getLevel(); + final QuoteSide side = msg.getSide(); + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final BookUpdateAction action = msg.getAction(); + + final Option>> exchange = getExchanges().getById(exchangeId); + + if (!exchange.hasValue() || exchange.get().getProcessor().isEmpty() || + exchange.get().getProcessor().isWaitingForSnapshot()) { + return false; } - final BookUpdateAction bookUpdateAction = l2EntryUpdateInfo.getAction(); - - final short level = l2EntryUpdateInfo.getLevel(); - final QuoteSide side = l2EntryUpdateInfo.getSide(); - - final L2MarketSide marketSide = exchange.getMarketSide(side); + final L2MarketSide marketSide = exchange.get().getProcessor().getMarketSide(side); - // TODO check if overlay WHY -// if (depth < currentSize) { -// final T item = items[depth]; -// -// if ((item != null) && (item.getPrice() != event.getPrice())) { // check if overlay -// delete(depth); -// insert(depth, event); -// -// break; -// } else { -// update(depth, event); -// } -// } else { -// insert(depth, event); -// } - if (!marketSide.hasLevel(level)) { - return; // Stop processing if exchange don't know about quote level + if (marketSide.isInvalidUpdate(action, level, msg.getPrice(), msg.getSize(), exchangeId)) { + if (validationOptions.isQuoteUpdate()) { + unmapQuote(exchangeId); + exchange.get().getProcessor().processL2EntryUpdate(pck, msg); + } + return false; } + final BookUpdateAction bookUpdateAction = msg.getAction(); + if (bookUpdateAction == BookUpdateAction.DELETE) { - final Quote remove = marketSide.getQuote(level); - removeQuote(remove, side); + final Quote quote = marketSide.getQuote(level); + removeQuote(quote, side); } else if (bookUpdateAction == BookUpdateAction.UPDATE) { final Quote quote = marketSide.getQuote(level); - updateQuote(quote, side, l2EntryUpdateInfo); + updateQuote(quote, side, msg); } - exchange.processL2EntryUpdateInfo(l2EntryUpdateInfo); + return exchange.get().getProcessor().processL2EntryUpdate(pck, msg); } + protected abstract void updateQuote(final Quote previous, + final QuoteSide side, + final L2EntryUpdateInfo update); - @Override - public boolean isWaitingForSnapshot() { - return isWaitingForSnapshot; - } - - private void waitingForSnapshot() { - if (!isWaitingForSnapshot()) { - isWaitingForSnapshot = true; - } - } - - private void notWaitingForSnapshot() { - if (isWaitingForSnapshot()) { - isWaitingForSnapshot = false; - } - } - - abstract void updateQuote(final Quote previous, - final QuoteSide side, - final L2EntryUpdateInfo update); - - public void insertAll(final L2Processor exchange, final QuoteSide side) { + private void mapQuote(final L2Processor exchange, final QuoteSide side) { final L2MarketSide marketSide = exchange.getMarketSide(side); for (int i = 0; i < marketSide.depth(); i++) { final Quote insert = marketSide.getQuote(i); @@ -237,13 +240,13 @@ public void insertAll(final L2Processor exchange, final QuoteSide side) { } } - public Quote insertQuote(final Quote insert, final QuoteSide side) { + private Quote insertQuote(final Quote insert, final QuoteSide side) { return insertQuote(insert, getMarketSide(side)); } - abstract Quote insertQuote(final Quote insert, final L2MarketSide marketSide); + protected abstract Quote insertQuote(final Quote insert, final L2MarketSide marketSide); - public void removeAll(final L2Processor exchange, final QuoteSide side) { + protected void removeAll(final L2Processor exchange, final QuoteSide side) { final MarketSide marketSide = exchange.getMarketSide(side); for (int i = 0; i < marketSide.depth(); i++) { final Quote remove = marketSide.getQuote(i); @@ -251,35 +254,38 @@ public void removeAll(final L2Processor exchange, final QuoteSide side) { } } - public void removeQuote(final Quote remove, final QuoteSide side) { + private void removeQuote(final Quote remove, final QuoteSide side) { final L2MarketSide marketSide = getMarketSide(side); removeQuote(remove, marketSide); } - abstract boolean removeQuote(final Quote remove, final L2MarketSide marketSide); + protected abstract boolean removeQuote(Quote remove, L2MarketSide marketSide); - abstract L2Processor clearExchange(final L2Processor exchange); - - public L2Processor clearExchange(final long exchangeId) { - final L2Processor exchange = getOrCreateExchange(exchangeId); - return clearExchange(exchange); + protected L2Processor unmapQuote(final long exchangeId) { + final L2Processor exchange = getOrCreateExchange(exchangeId).get().getProcessor(); + return unmapQuote(exchange); } + protected abstract L2Processor unmapQuote(L2Processor exchange); + /** * Get stock exchange holder by id(create new if it does not exist). * * @param exchangeId - id of exchange. * @return exchange book by id. */ - public L2Processor getOrCreateExchange(final long exchangeId) { + private Option>> getOrCreateExchange(@Alphanumeric final long exchangeId) { + if (!AlphanumericUtils.isValidAlphanumeric(exchangeId) || TypeConstants.EXCHANGE_NULL == exchangeId) { + //TODO LOG warning + return Option.empty(); + } final MutableExchangeList>> exchanges = this.getExchanges(); - Option>> exchangeHolder = exchanges.getById(exchangeId); - if (!exchangeHolder.hasValue()) { - final L2SingleExchangeQuoteProcessor processor = - new L2SingleExchangeQuoteProcessor<>(exchangeId, initialDepth, maxDepth, pool, gapMode, updateMode, unreachableDepthMode); + Option>> holder = exchanges.getById(exchangeId); + if (!holder.hasValue()) { + final L2Processor processor = new L2SingleExchangeQuoteProcessor<>(options, pool, exchangeId); exchanges.add(new MutableExchangeImpl<>(exchangeId, processor)); - exchangeHolder = exchanges.getById(exchangeId); + holder = exchanges.getById(exchangeId); } - return exchangeHolder.get().getProcessor(); + return holder; } } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL3MarketSide.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL3MarketSide.java new file mode 100644 index 0000000..c1ba577 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/AbstractL3MarketSide.java @@ -0,0 +1,317 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.impl; + +import com.epam.deltix.dfp.Decimal; +import com.epam.deltix.dfp.Decimal64Utils; +import com.epam.deltix.orderbook.core.api.EntryValidationCode; +import com.epam.deltix.orderbook.core.impl.collections.rbt.RBTree; +import com.epam.deltix.timebase.messages.universal.InsertType; +import com.epam.deltix.timebase.messages.universal.QuoteSide; +import com.epam.deltix.util.collections.CharSeqToObjMap; + +import java.util.*; + +import static com.epam.deltix.dfp.Decimal64Utils.*; +import static com.epam.deltix.orderbook.core.api.EntryValidationCode.*; + +/** + * @author Andrii_Ostapenko1 + */ +abstract class AbstractL3MarketSide implements L3MarketSide { + + protected final RBTree data; + private final CharSeqToObjMap quoteHashMap; + private final ReusableIterator itr; + // This parameter is used to limit maximum elements and to understand whether the side is full or not. + private final int maxDepth; + private long virtualClock; + + AbstractL3MarketSide(final int initialCapacity, final int maxDepth) { + this.maxDepth = maxDepth; + this.data = new RBTree<>(initialCapacity, new QuoteComparator()); + this.itr = new ReusableIterator<>(); + this.quoteHashMap = new CharSeqToObjMap<>(); + virtualClock = 0; + } + + @Override + public int getMaxDepth() { + return maxDepth; + } + + @Override + public int depth() { + return data.size(); + } + + @Override + public long getTotalQuantity() { + @Decimal long result = ZERO; + for (final Quote quote : this) { + result = Decimal64Utils.add(result, quote.getSize()); + } + return result; + } + + /** + * Clears the market side in linear time + */ + @Override + public void clear() { + data.clear(); + quoteHashMap.clear(); + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public Quote getQuote(final int level) { + throw new UnsupportedOperationException(); + } + + @Override + public Quote getQuote(final CharSequence quoteId) { + return quoteHashMap.get(quoteId, null); + } + + @Override + public boolean add(final Quote insert) { + if (quoteHashMap.put(insert.getQuoteId(), insert)) { + insert.setSequenceNumber(virtualClock++); + data.put(insert, insert); + return true; + } + return false; + } + + @Override + public Quote remove(final CharSequence quoteId) { + final Quote result = quoteHashMap.remove(quoteId, null); + if (result != null) { + data.remove(result); + } + return result; + } + + @Override + public Quote remove(final Quote quote) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isFull() { + return depth() == maxDepth; + } + + @Override + public Quote getBestQuote() { + if (isEmpty()) { + return null; + } + return data.firstKey(); + } + + @Override + public boolean hasLevel(final int level) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasQuote(final CharSequence quoteId) { + return quoteHashMap.containsKey(quoteId); + } + + @Override + public Quote getWorstQuote() { + if (isEmpty()) { + return null; + } + return data.lastKey(); + } + + /** + * @return error code, or null if everything is valid + */ + @Override + public EntryValidationCode isInvalidInsert(final InsertType type, + final CharSequence quoteId, + final @Decimal long price, + final @Decimal long size, + final QuoteSide side) { + if (type != InsertType.ADD_BACK) { + return UNSUPPORTED_INSERT_TYPE; + } + + if (side == null) { + return UNSPECIFIED_SIDE; + } + + if (quoteId == null || quoteId.length() == 0) { + return MISSING_QUOTE_ID; + } + + if (isNaN(price)) { + return MISSING_PRICE; + } + + if (isLessOrEqual(size, ZERO)) { + return BAD_SIZE; + } + + return null; // all good + } + + /** + * @return error code, or null if everything is valid + */ + @Override + public EntryValidationCode isInvalidUpdate(final Quote quote, + final CharSequence quoteId, + final @Decimal long price, + final @Decimal long size, + final QuoteSide side) { + if (side == null) { + return UNSPECIFIED_SIDE; + } + + if (quoteId == null || quoteId.length() == 0) { + return MISSING_QUOTE_ID; + } + + if (quote == null) { + return UNKNOWN_QUOTE_ID; + } + + if (isNotEqual(quote.getPrice(), price)) { + return MODIFY_CHANGE_PRICE; + } + + if (isLessOrEqual(size, ZERO)) { + return BAD_SIZE; + } + + if (Decimal64Utils.isLess(quote.getSize(), size)) { + return MODIFY_INCREASE_SIZE; + } + + return null; // all good + } + + @Override + public void buildFromSorted(final ArrayList quotes) { + data.buildFromSorted(quotes); + final int len = quotes.size(); + for (int i = 0; i < len; i++) { + final Quote quote = quotes.get(i); + quoteHashMap.put(quote.getQuoteId(), quote); + } + virtualClock = data.size(); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + for (final Quote quote : this) { + builder.append(quote).append("\n"); + } + return builder.toString(); + } + + @Override + public Iterator iterator(final int fromLevel, final int toLevel) { + if (fromLevel != 0) { + throw new UnsupportedOperationException(); + } + itr.iterateBy(data); + return itr; + } + + /** + * An adapter to safely externalize the value iterator. + */ + static final class ReusableIterator implements Iterator { + + private Iterator> iterator; + + private void iterateBy(final RBTree tm) { + Objects.requireNonNull(tm); + iterator = tm.iterator(); + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Quote next() { + return iterator.next().getValue(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read only iterator"); + } + } + + static class ASKS extends AbstractL3MarketSide { + + ASKS(final int initialDepth, final int maxDepth) { + super(initialDepth, maxDepth); + } + + @Override + public QuoteSide getSide() { + return QuoteSide.ASK; + } + + } + + static class BIDS extends AbstractL3MarketSide { + + BIDS(final int initialDepth, final int maxDepth) { + super(initialDepth, maxDepth); + } + + @Override + public QuoteSide getSide() { + return QuoteSide.BID; + } + + } + + class QuoteComparator implements Comparator { + + @Override + public int compare(final Quote o1, final Quote o2) { + final int priceComp = Decimal64Utils.compareTo(o1.getPrice(), o2.getPrice()); + if (priceComp == 0) { + return Long.compare(o1.getSequenceNumber(), o2.getSequenceNumber()); + } + if (getSide() == QuoteSide.ASK) { + return priceComp; + } else { + return -priceComp; + } + } + } + +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactAbstractL2MarketSide.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactAbstractL2MarketSide.java new file mode 100644 index 0000000..1d77f51 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactAbstractL2MarketSide.java @@ -0,0 +1,313 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.impl; + +import com.epam.deltix.dfp.Decimal; +import com.epam.deltix.dfp.Decimal64Utils; +import com.epam.deltix.orderbook.core.api.MarketSide; +import com.epam.deltix.timebase.messages.universal.BookUpdateAction; +import com.epam.deltix.timebase.messages.universal.QuoteSide; +import com.epam.deltix.util.annotations.Alphanumeric; + +import java.util.Iterator; +import java.util.Objects; + +import static com.epam.deltix.dfp.Decimal64Utils.*; +import static com.epam.deltix.timebase.messages.TypeConstants.EXCHANGE_NULL; + +/** + * @author Andrii_Ostapenko1 + */ +abstract class CompactAbstractL2MarketSide implements CompactL2MarketSide { + + protected final long[] data; + private final Quote holder = (Quote) new MutableOrderBookQuoteImpl(); + private final ReusableIterator itr; + // This parameter is used to limit maximum elements and to understand whether the side is full or not. + private final int maxDepth; + private int depth; + + CompactAbstractL2MarketSide(final int maxDepth) { + this.maxDepth = maxDepth; + this.data = new long[maxDepth << 1]; + depth = 0; + this.itr = new ReusableIterator<>(); + } + + @Override + public int getMaxDepth() { + return maxDepth; + } + + @Override + public int depth() { + return depth; + } + + @Override + public long getTotalQuantity() { + @Decimal long result = ZERO; + final int len = depth << 1; + for (int i = 1; i < len; i += 2) { + result = Decimal64Utils.add(result, data[i]); + } + return result; + } + + @Override + public void clear() { + depth = 0; + } + + @Override + public boolean isEmpty() { + return depth == 0; + } + + @Override + public Quote getQuote(final int level) { + if (!hasLevel(level)) { + return null; + } + final int idx = level << 1; + holder.setPrice(data[idx]); + holder.setSize(data[idx + 1]); + return holder; + } + + @Override + public void add(final int level, final long price, final long size) { + final int idx = level << 1; + if (level < depth) { + System.arraycopy(data, idx, data, idx + 2, (depth << 1) - idx); + } + data[idx] = price; + data[idx + 1] = size; + ++depth; + } + + public void set(final int level, final long price, final long size) { + if (level == depth) { + ++depth; + } + final int idx = level << 1; + data[idx] = price; + data[idx + 1] = size; + } + + @Override + public void remove(final int level) { + if (hasLevel(level)) { + final int idx = level << 1; + if (level < depth) { + System.arraycopy(data, idx + 2, data, idx, (depth << 1) - idx - 2); + } + --depth; + } + } + + @Override + public boolean hasLevel(final int level) { + return depth > level && level >= 0; + } + + @Override + public void removeWorstQuote() { + --depth; + } + + @Override + public boolean isFull() { + return depth == maxDepth; + } + + @Override + public boolean isGap(final int level) { + return !hasLevel(level) && level > depth; + } + + @Override + public boolean isUnreachableLeve(int level) { + return level < 0 || level >= maxDepth; + } + + @Override + public Quote getBestQuote() { + return getQuote(0); + } + + @Override + public Quote getWorstQuote() { + return getQuote(depth - 1); + } + + @Override + public boolean isInvalidInsert(final int level, + final @Decimal long price, + final @Decimal long size, + final @Alphanumeric long exchangeId) { + if (level < 0 || isEqual(price, NULL) || isLessOrEqual(size, ZERO) || exchangeId == EXCHANGE_NULL) { + return true; + } + if (isUnreachableLeve(level)) { + return true; + } + if (isGap(level)) { + return true; + } + return !checkOrderPrice(level, price); + } + + @Override + public boolean isInvalidUpdate(final BookUpdateAction action, + final int level, + final @Decimal long price, + final @Decimal long size, + final @Alphanumeric long exchangeId) { + if (!hasLevel(level)) { + return true; + } + if (action != BookUpdateAction.DELETE) { + return isNotEqual(data[level << 1], price) || isLess(size, ZERO); + } + return false; + } + + @Override + public boolean checkOrderPrice(final int level, final @Decimal long price) { + + @Decimal final long previousPrice = hasLevel(level - 1) ? data[(level - 1) << 1] : NULL; + @Decimal final long nextPrice = hasLevel(level) ? data[level << 1] : NULL; + + boolean badState = false; + if (getSide() == QuoteSide.ASK) { + if (isNotEqual(previousPrice, NULL) && isGreater(previousPrice, price)) { + badState = true; + } + if (isNotEqual(nextPrice, NULL) && isLess(nextPrice, price)) { + badState = true; + } + } else { + if (isNotEqual(previousPrice, NULL) && isLess(previousPrice, price)) { + badState = true; + } + if (isNotEqual(nextPrice, NULL) && isGreater(nextPrice, price)) { + badState = true; + } + } + return !badState; + } + + @Override + public boolean validateState() { + if (isEmpty()) { + return true; + } + final int len = depth << 1; + for (int i = 0; i < len; i += 2) { + if (isInvalidInsert(i >> 1, data[i], data[i + 1], EXCHANGE_NULL ^ 1)) { + return false; + } + } + return true; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + for (final Quote quote : this) { + builder.append(quote).append(" "); + } + return builder.toString(); + } + + @Override + public Iterator iterator(final int fromLevel, final int toLevel) { + itr.iterateBy(this, fromLevel, toLevel); + return itr; + } + + /** + * An adapter to safely externalize the value iterator. + */ + static final class ReusableIterator implements Iterator { + + /** + * Index of element to be returned by subsequent call to next. + */ + private int cursor; + + private int end; + + private MarketSide marketSide; + + private void iterateBy(final MarketSide marketSide, final int cursor, final int end) { + Objects.requireNonNull(marketSide); + this.marketSide = marketSide; + this.cursor = cursor; + if (end > marketSide.depth() || end < 0) { + this.end = marketSide.depth(); + } else { + this.end = end; + } + if (cursor > end || cursor < 0) { + this.cursor = end; + } + } + + @Override + public boolean hasNext() { + return cursor != end; + } + + @Override + public Quote next() { + return marketSide.getQuote(cursor++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read only iterator"); + } + } + + static class ASK extends CompactAbstractL2MarketSide { + + ASK(final int maxDepth) { + super(maxDepth); + } + + @Override + public QuoteSide getSide() { + return QuoteSide.ASK; + } + + } + + static class BID extends CompactAbstractL2MarketSide { + + BID(final int maxDepth) { + super(maxDepth); + } + + @Override + public QuoteSide getSide() { + return QuoteSide.BID; + } + + } +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactL2MarketSide.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactL2MarketSide.java new file mode 100644 index 0000000..0d42de8 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactL2MarketSide.java @@ -0,0 +1,160 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.impl; + +import com.epam.deltix.dfp.Decimal; +import com.epam.deltix.orderbook.core.api.MarketSide; +import com.epam.deltix.timebase.messages.universal.BookUpdateAction; +import com.epam.deltix.timebase.messages.universal.QuoteSide; +import com.epam.deltix.util.annotations.Alphanumeric; + +import java.util.Objects; + +/** + * @author Andrii_Ostapenko1 + */ +interface CompactL2MarketSide extends MarketSide { + + static CompactL2MarketSide factory(final int maxDepth, + final QuoteSide side) { + Objects.requireNonNull(side); + switch (side) { + case BID: + return new CompactAbstractL2MarketSide.BID<>(maxDepth); + case ASK: + return new CompactAbstractL2MarketSide.ASK<>(maxDepth); + default: + throw new IllegalStateException("Unexpected value: " + side); + } + } + + @Override + default Quote getQuote(final CharSequence quoteId) { + // Not supported for L2 + return null; + } + + @Override + default boolean hasQuote(final CharSequence quoteId) { + // Not supported for L2 + return false; + } + + /** + * Inserts the specified quote at the specified level and shifts the quotes right. + * + * @param level - the level at which quote needs to be inserted. + * @param price - the price of the quote. + * @param size - the size of the quote. + */ + void add(int level, long price, long size); + + /** + * Returns the maximum depth of this market side. + * + * @return the maximum depth. + */ + int getMaxDepth(); + + /** + * Removes the worst quote from this market side. + */ + void removeWorstQuote(); + + /** + * Remove quote by level. + * Shifts any subsequent elements to the left. + * + * @param level - the level of the quote to be removed + */ + void remove(int level); + + /** + * Returns true if this market side if full. + * + * @return true if this market side if full. + */ + boolean isFull(); + + /** + * Verifies ability insert or update quote by market side. + * + * @param level - quote level to use + * @return true if this quote level is unexpected + */ + boolean isGap(int level); + + + boolean isUnreachableLeve(int level); + + /** + * Checks if the specified price is sorted. + * + * @param level - quote level to use + * @param price - price to be checked + * @return true if this price is sorted. + */ + boolean checkOrderPrice(int level, @Decimal long price); + + void clear(); + + /** + * Validates the state of this market side. + * + * @return true if the market side state is valid, false otherwise. + */ + boolean validateState(); + + /** + * Sets a quote at given level with provided price and size. + * + * @param level - level at which the quote should be set. + * @param price - price of the quote. + * @param size - size of the quote. + */ + void set(int level, long price, long size); + + /** + * Checks if inserting a quote at given level with provided price, size and exchangeId would be invalid. + * + * @param level - level to check + * @param price - price to check + * @param size - size to check + * @param exchangeId - exchangeId to check + * @return true if the insert operation would be invalid, false otherwise. + */ + boolean isInvalidInsert(int level, + @Decimal long price, + @Decimal long size, + @Alphanumeric long exchangeId); + + /** + * Checks if updating a quote with given action, level, price, size and exchangeId would be invalid. + * + * @param action - action to check + * @param level - level to check + * @param price - price to check + * @param size - size to check + * @param exchangeId - exchangeId to check + * @return true if the update operation would be invalid, false otherwise. + */ + boolean isInvalidUpdate(BookUpdateAction action, + int level, + @Decimal long price, + @Decimal long size, + @Alphanumeric long exchangeId); +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactL2Processor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactL2Processor.java new file mode 100644 index 0000000..34ed9b2 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactL2Processor.java @@ -0,0 +1,96 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.impl; + + +import com.epam.deltix.timebase.messages.universal.*; +import com.epam.deltix.util.collections.generated.ObjectList; + +/** + * Processor for L2 universal market data format that included in package (Package Header). + * + * @author Andrii_Ostapenko + */ +interface CompactL2Processor extends QuoteProcessor { + + @Override + default DataModelType getQuoteLevels() { + return DataModelType.LEVEL_TWO; + } + + @Override + CompactL2MarketSide getMarketSide(QuoteSide side); + + @Override + MutableExchangeList>> getExchanges(); + + /** + * This type reports incremental Level2-new: insert one line in Order Book either on ask or bid side. + * + * @param pck + * @param msg - Level2-new entry + * @return insert quote + */ + Quote processL2EntryNew(PackageHeaderInfo pck, L2EntryNewInfo msg); + + /** + * This type reports incremental Level2-update: update or delete one line in Order Book either on ask or bid side. + * + * @param pck + * @param msg - Level2-update + * @return true if quote was updated, false if quote was not found + */ + boolean processL2EntryUpdate(PackageHeaderInfo pck, L2EntryUpdateInfo msg); + + boolean processL2Snapshot(PackageHeaderInfo msg); + + boolean isWaitingForSnapshot(); + + boolean isSnapshotAllowed(PackageHeaderInfo msg); + + default boolean processIncrementalUpdate(PackageHeaderInfo pck, final BaseEntryInfo entryInfo) { + if (entryInfo instanceof L2EntryNew) { + final L2EntryNew entry = (L2EntryNew) entryInfo; + return processL2EntryNew(pck, entry) != null; + } else if (entryInfo instanceof L2EntryUpdate) { + final L2EntryUpdate entry = (L2EntryUpdate) entryInfo; + return processL2EntryUpdate(pck, entry); + } else if (entryInfo instanceof StatisticsEntry) { + return true; + } + return false; + } + + default boolean processSnapshot(final PackageHeaderInfo msg) { + final ObjectList entries = msg.getEntries(); + final int n = entries.size(); + // skip statistic entries try to establish if we are dealing with order book reset or normal snapshot + for (int i = 0; i < n; i++) { + final BaseEntryInfo entry = entries.get(i); + if (entry instanceof L2EntryNewInfo) { + return processL2Snapshot(msg); + } else if (entry instanceof BookResetEntryInfo) { + final BookResetEntryInfo resetEntry = (BookResetEntryInfo) entry; + if (resetEntry.getModelType() == getQuoteLevels()) { + return processBookResetEntry(msg, resetEntry); + } + } + } + return false; + } + +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactL2SingleExchangeQuoteProcessor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactL2SingleExchangeQuoteProcessor.java new file mode 100644 index 0000000..ea11044 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/CompactL2SingleExchangeQuoteProcessor.java @@ -0,0 +1,283 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.impl; + +import com.epam.deltix.containers.AlphanumericUtils; +import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.timebase.messages.TypeConstants; +import com.epam.deltix.timebase.messages.service.FeedStatus; +import com.epam.deltix.timebase.messages.service.SecurityFeedStatusMessage; +import com.epam.deltix.timebase.messages.universal.*; +import com.epam.deltix.util.annotations.Alphanumeric; +import com.epam.deltix.util.collections.generated.ObjectList; + +import static com.epam.deltix.timebase.messages.universal.QuoteSide.ASK; +import static com.epam.deltix.timebase.messages.universal.QuoteSide.BID; + + +/** + * @author Andrii_Ostapenko1 + */ +public class CompactL2SingleExchangeQuoteProcessor implements CompactL2Processor { + + protected final CompactL2MarketSide bids; + protected final CompactL2MarketSide asks; + private final MutableExchangeList>> exchanges; + + private final EventHandler eventHandler; + + //Parameters + private final ValidationOptions validationOptions; + private final DisconnectMode disconnectMode; + + public CompactL2SingleExchangeQuoteProcessor(final OrderBookOptions options) { + this.disconnectMode = options.getDisconnectMode().orElse(Defaults.DISCONNECT_MODE); + this.validationOptions = options.getInvalidQuoteMode().orElse(Defaults.VALIDATION_OPTIONS); + this.eventHandler = new EventHandlerImpl(options); + + this.exchanges = new MutableExchangeListImpl<>(); + + final int maxDepth = options.getMaxDepth().orElse(Defaults.MAX_DEPTH); + this.asks = CompactL2MarketSide.factory(maxDepth, ASK); + this.bids = CompactL2MarketSide.factory(maxDepth, BID); + } + + @Override + public String getDescription() { + return "Compact L2/Single exchange"; + } + + @Override + public Quote processL2EntryNew(final PackageHeaderInfo pck, final L2EntryNewInfo msg) { + final long exchangeId = msg.getExchangeId(); + final Option>> exchange = getOrCreateExchange(exchangeId); + if (!exchange.hasValue()) { + return null; + } + + if (exchange.get().getProcessor().isWaitingForSnapshot()) { + return null; + } + + final QuoteSide side = msg.getSide(); + final CompactL2MarketSide marketSide = exchange.get().getProcessor().getMarketSide(side); + final int level = msg.getLevel(); + + if (marketSide.isInvalidInsert(level, msg.getPrice(), msg.getSize(), exchangeId)) { + if (validationOptions.isQuoteInsert()) { + clear(); + eventHandler.onBroken(); + } + return null; + } + + if (marketSide.isFull()) { + marketSide.removeWorstQuote(); + } + marketSide.add(level, msg.getPrice(), msg.getSize()); + return marketSide.getQuote(level); + } + + @Override + public boolean processL2EntryUpdate(final PackageHeaderInfo pck, final L2EntryUpdateInfo msg) { + final QuoteSide side = msg.getSide(); + final int level = msg.getLevel(); + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final BookUpdateAction action = msg.getAction(); + + final Option>> exchange = getExchanges().getById(exchangeId); + if (!exchange.hasValue()) { + return false; + } + + if (exchange.get().getProcessor().isWaitingForSnapshot()) { + return false; + } + + final CompactL2MarketSide marketSide = exchange.get().getProcessor().getMarketSide(side); + if (marketSide.isInvalidUpdate(action, level, msg.getPrice(), msg.getSize(), exchangeId)) { + if (validationOptions.isQuoteUpdate()) { + clear(); + eventHandler.onBroken(); + return false; + } + return true; // skip invalid update + } + + if (action == BookUpdateAction.DELETE) { + marketSide.remove(level); + } else if (action == BookUpdateAction.UPDATE) { + marketSide.set(level, msg.getPrice(), msg.getSize()); + } + return true; + } + + @Override + public boolean processL2Snapshot(final PackageHeaderInfo pck) { + if (!isSnapshotAllowed(pck)) { + return false; + } + + final ObjectList entries = pck.getEntries(); + + clear(); + int askCnt = 0; + int bidCnt = 0; + for (int i = 0; i < entries.size(); i++) { + final BaseEntryInfo e = entries.get(i); + if (e instanceof L2EntryNewInterface) { + final L2EntryNewInterface entry = (L2EntryNewInterface) e; + + final int level = entry.getLevel(); + final QuoteSide side = entry.getSide(); + @Alphanumeric final long exchangeId = entry.getExchangeId(); + + // We expect that exchangeId is valid and all entries have the same exchangeId + final Option>> exchange = getOrCreateExchange(exchangeId); + if (!exchange.hasValue()) { + clear(); + eventHandler.onBroken(); + return false; + } + + final CompactL2MarketSide marketSide = exchange.get().getProcessor().getMarketSide(side); + + // Both side have the same max depth + final int maxDepth = marketSide.getMaxDepth(); + if ((side == ASK && askCnt == maxDepth) || (side == BID && bidCnt == maxDepth)) { + continue; + } + marketSide.add(level, entry.getPrice(), entry.getSize()); + + if (side == ASK) { + askCnt++; + } else { + bidCnt++; + } + + if (askCnt == maxDepth && bidCnt == maxDepth) { + break; + } + } + } + + //Validate state after snapshot + //We believe that snapshot is valid, but... + if (!asks.validateState() || !bids.validateState()) { + clear(); + eventHandler.onBroken(); + return false; + } + + eventHandler.onSnapshot(); + return true; + } + + @Override + public boolean processBookResetEntry(final PackageHeaderInfo pck, final BookResetEntryInfo msg) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final Option>> exchange = getOrCreateExchange(exchangeId); + + if (exchange.hasValue()) { + clear(); + eventHandler.onReset(); + return true; + } else { + return false; + } + } + + @Override + public boolean processSecurityFeedStatus(final SecurityFeedStatusMessage msg) { + if (msg.getStatus() == FeedStatus.NOT_AVAILABLE) { + if (disconnectMode == DisconnectMode.CLEAR_EXCHANGE) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final Option>> exchange = getOrCreateExchange(exchangeId); + if (exchange.hasValue()) { + clear(); + eventHandler.onDisconnect(); + return true; + } + } + } + return false; + } + + @Override + public MutableExchangeList>> getExchanges() { + return exchanges; + } + + @Override + public CompactL2MarketSide getMarketSide(final QuoteSide side) { + return side == BID ? bids : asks; + } + + @Override + public void clear() { + releaseAndClean(asks); + releaseAndClean(bids); + } + + @Override + public boolean isEmpty() { + return asks.isEmpty() && bids.isEmpty(); + } + + /** + * Check if snapshot is available for processing. + * + * @param msg - snapshot message + * @return true if snapshot is available for processing + */ + @Override + public boolean isSnapshotAllowed(final PackageHeaderInfo msg) { + final PackageType type = msg.getPackageType(); + return eventHandler.isSnapshotAllowed(type); + } + + @Override + public boolean isWaitingForSnapshot() { + return eventHandler.isWaitingForSnapshot(); + } + + private void releaseAndClean(final CompactL2MarketSide side) { + side.clear(); + } + + /** + * Get stock exchange holder by id(create new if it does not exist). + * You can create only one exchange. + * + * @param exchangeId - id of exchange. + * @return exchange book by id. + */ + private Option>> getOrCreateExchange(@Alphanumeric final long exchangeId) { + if (!AlphanumericUtils.isValidAlphanumeric(exchangeId) || TypeConstants.EXCHANGE_NULL == exchangeId) { + return Option.empty(); + } + + if (!exchanges.isEmpty()) { + return exchanges.getById(exchangeId); + } + final MutableExchange> exchange = new MutableExchangeImpl<>(exchangeId, this); + exchanges.add(exchange); + return exchanges.getById(exchangeId); + } + +} + diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/EventHandler.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/EventHandler.java new file mode 100644 index 0000000..b0efb60 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/EventHandler.java @@ -0,0 +1,43 @@ +package com.epam.deltix.orderbook.core.impl; + + +import com.epam.deltix.timebase.messages.universal.PackageType; + +/** + * Callback interface to be implemented for processing events as they become available in the {@link deltix.orderbook.core.api.OrderBook} + */ +interface EventHandler { + /** + * Check if snapshot type is available for processing. + * + * @param type snapshot type + * @return true if snapshot is available for processing + */ + boolean isSnapshotAllowed(PackageType type); + + /** + * @return true if we can't process incremental update messages but waiting for next snapshot message + */ + boolean isWaitingForSnapshot(); + + /** + * Call this method when book lost connection with feed. + */ + void onDisconnect(); + + /** + * Call this method when book received reset entry. + */ + void onReset(); + + /** + * Call this method when book received snapshot. + */ + void onSnapshot(); + + + /** + * Call this method when book received invalid date. + */ + void onBroken(); +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/EventHandlerImpl.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/EventHandlerImpl.java new file mode 100644 index 0000000..f17fdd2 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/EventHandlerImpl.java @@ -0,0 +1,79 @@ +package com.epam.deltix.orderbook.core.impl; + + +import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.timebase.messages.universal.PackageType; + +import static com.epam.deltix.timebase.messages.universal.PackageType.PERIODICAL_SNAPSHOT; +import static com.epam.deltix.timebase.messages.universal.PackageType.VENDOR_SNAPSHOT; + +class EventHandlerImpl implements EventHandler { + + /** + * This parameter using for handle periodical snapshot entry and depends on {@link PeriodicalSnapshotMode}. + */ + private boolean isPeriodicalSnapshotAllowed; + + /** + * This parameter using for handle incremental update entry and depends on {@link UpdateMode} and {@link ResetMode}. + */ + private boolean isWaitingForSnapshot; + + //Parameters + private final UpdateMode updateMode; + private final ResetMode resetMode; + private final PeriodicalSnapshotMode periodicalSnapshotMode; + private final DisconnectMode disconnectMode; + + EventHandlerImpl(final OrderBookOptions options) { + this.disconnectMode = options.getDisconnectMode().orElse(Defaults.DISCONNECT_MODE); + this.updateMode = options.getUpdateMode().orElse(Defaults.UPDATE_MODE); + this.resetMode = options.getResetMode().orElse(Defaults.RESET_MODE); + this.periodicalSnapshotMode = options.getPeriodicalSnapshotMode().orElse(Defaults.PERIODICAL_SNAPSHOT_MODE); + + this.isPeriodicalSnapshotAllowed = periodicalSnapshotMode != PeriodicalSnapshotMode.SKIP_ALL; + this.isWaitingForSnapshot = (updateMode == UpdateMode.WAITING_FOR_SNAPSHOT); + } + + @Override + public boolean isSnapshotAllowed(final PackageType type) { + if (type == null) { + return false; + } + + if (type == VENDOR_SNAPSHOT) { + return true; + } + + return isPeriodicalSnapshotAllowed && type == PERIODICAL_SNAPSHOT; + } + + @Override + public boolean isWaitingForSnapshot() { + return isWaitingForSnapshot; + } + + @Override + public void onDisconnect() { + isWaitingForSnapshot = (updateMode == UpdateMode.WAITING_FOR_SNAPSHOT); //TODO: review: switch to (disconnectMode == CLEAR_EXCHANGE) ? + isPeriodicalSnapshotAllowed = periodicalSnapshotMode != PeriodicalSnapshotMode.SKIP_ALL; + } + + @Override + public void onReset() { + isWaitingForSnapshot = (resetMode == ResetMode.WAITING_FOR_SNAPSHOT); + isPeriodicalSnapshotAllowed = periodicalSnapshotMode != PeriodicalSnapshotMode.SKIP_ALL; + } + + @Override + public void onSnapshot() { + isWaitingForSnapshot = false; + isPeriodicalSnapshotAllowed = periodicalSnapshotMode == PeriodicalSnapshotMode.PROCESS_ALL; + } + + @Override + public void onBroken() { + isWaitingForSnapshot = true; + isPeriodicalSnapshotAllowed = periodicalSnapshotMode != PeriodicalSnapshotMode.SKIP_ALL; + } +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1MarketSide.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1MarketSide.java index 7ad3a2a..61e2a07 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1MarketSide.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1MarketSide.java @@ -16,6 +16,7 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.orderbook.core.api.MarketSide; import com.epam.deltix.timebase.messages.universal.QuoteSide; @@ -38,6 +39,18 @@ static L1MarketSide factory(final Q } } + @Override + default Quote getQuote(final CharSequence quoteId) { + // Not supported for L1 + return null; + } + + @Override + default boolean hasQuote(final CharSequence quoteId) { + // Not supported for L1 + return false; + } + void insert(Quote insert); void clear(); diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1OrderBookFactory.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1OrderBookFactory.java index c32bd44..822db86 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1OrderBookFactory.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1OrderBookFactory.java @@ -16,10 +16,10 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.orderbook.core.api.OrderBook; import com.epam.deltix.orderbook.core.api.OrderBookQuote; -import com.epam.deltix.orderbook.core.options.Option; -import com.epam.deltix.orderbook.core.options.UpdateMode; +import com.epam.deltix.orderbook.core.options.OrderBookOptions; /** * A factory that implements order book for Level1. @@ -32,19 +32,27 @@ @SuppressWarnings("unchecked") public class L1OrderBookFactory { + /** + * Prevents instantiation + */ + protected L1OrderBookFactory() { + } + /** * Creates OrderBook for single exchange market feed. * - * @param - type of quote - * @param symbol - type of symbol - * @param updateMode - modes of order book work. Waiting first snapshot don't apply incremental updates before it or no. + * @param - type of quote. + * @param options - to use. * @return order book */ - public static OrderBook newSingleExchangeBook(final Option symbol, final UpdateMode updateMode) { + public static OrderBook newSingleExchangeBook(final OrderBookOptions options) { final int initialSize = 2; - final ObjectPool pool = new ObjectPool<>(initialSize, MutableOrderBookQuoteImpl::new); - final QuoteProcessor processor = new L1SingleExchangeQuoteProcessor<>(pool, updateMode); - return (OrderBook) new OrderBookDecorator<>(symbol, processor); + + final ObjectPool pool = (ObjectPool) + options.getSharedObjectPool().orElse(QuotePoolFactory.create(options, initialSize)); + + final QuoteProcessor processor = new L1SingleExchangeQuoteProcessor<>(options, pool); + return (OrderBook) new OrderBookDecorator<>(options.getSymbol(), processor); } } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1Processor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1Processor.java index 1f1c2e1..4659030 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1Processor.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1Processor.java @@ -16,6 +16,7 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.timebase.messages.universal.*; import com.epam.deltix.util.collections.generated.ObjectList; @@ -24,7 +25,7 @@ * * @author Andrii_Ostapenko */ -interface L1Processor extends QuoteProcessor, ResetEntryProcessor { +interface L1Processor extends QuoteProcessor { @Override default DataModelType getQuoteLevels() { @@ -32,22 +33,26 @@ default DataModelType getQuoteLevels() { } @Override - L1MarketSide getMarketSide(final QuoteSide side); + L1MarketSide getMarketSide(QuoteSide side); /** * This type reports Level1-new: insert or update one line in Order Book either on ask or bid side. * + * @param pck - package header container * @param l1EntryNewInfo - Level1-new entry to use * @return insert quote */ - Quote processL1EntryNewInfo(final L1EntryInfo l1EntryNewInfo); + Quote processL1EntryNew(PackageHeaderInfo pck, L1EntryInfo l1EntryNewInfo); - void processL1VendorSnapshot(final PackageHeaderInfo marketMessageInfo); + boolean processL1Snapshot(PackageHeaderInfo marketMessageInfo); - default boolean process(final BaseEntryInfo pck) { - if (pck instanceof L1EntryInfo) { - final L1EntryInfo l1EntryNewInfo = (L1EntryInfo) pck; - processL1EntryNewInfo(l1EntryNewInfo); + default boolean processIncrementalUpdate(final PackageHeaderInfo pck, final BaseEntryInfo entryInfo) { + if (entryInfo instanceof L1EntryInfo) { + final L1EntryInfo l1EntryNewInfo = (L1EntryInfo) entryInfo; + //TODO need processing return value + processL1EntryNew(pck, l1EntryNewInfo); + return true; + } else if (entryInfo instanceof StatisticsEntry) { return true; } return false; @@ -55,15 +60,18 @@ default boolean process(final BaseEntryInfo pck) { default boolean processSnapshot(final PackageHeaderInfo marketMessageInfo) { final ObjectList entries = marketMessageInfo.getEntries(); - final BaseEntryInfo baseEntryInfo = entries.get(0); - if (baseEntryInfo instanceof L1EntryInfo) { - processL1VendorSnapshot(marketMessageInfo); - return true; - } else if (baseEntryInfo instanceof BookResetEntryInfo) { - final BookResetEntryInfo resetEntryInfo = (BookResetEntryInfo) baseEntryInfo; - if (resetEntryInfo.getModelType() == getQuoteLevels()) { - processBookResetEntry(resetEntryInfo); - return true; + final int n = entries.size(); + // skip statistic entries try to establish if we are dealing with order book reset or normal snapshot + for (int i = 0; i < n; i++) { + final BaseEntryInfo baseEntryInfo = entries.get(i); + if (baseEntryInfo instanceof L1EntryInfo) { + return processL1Snapshot(marketMessageInfo); + } else if (baseEntryInfo instanceof BookResetEntryInfo) { + final BookResetEntryInfo resetEntryInfo = (BookResetEntryInfo) baseEntryInfo; + if (resetEntryInfo.getModelType() == getQuoteLevels()) { + processBookResetEntry(marketMessageInfo, resetEntryInfo); + return true; + } } } return false; diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1SingleExchangeQuoteProcessor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1SingleExchangeQuoteProcessor.java index 7a10f58..994c513 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1SingleExchangeQuoteProcessor.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L1SingleExchangeQuoteProcessor.java @@ -16,10 +16,16 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.orderbook.core.api.ExchangeList; +import com.epam.deltix.orderbook.core.options.Defaults; +import com.epam.deltix.orderbook.core.options.DisconnectMode; import com.epam.deltix.orderbook.core.options.Option; -import com.epam.deltix.orderbook.core.options.UpdateMode; +import com.epam.deltix.orderbook.core.options.OrderBookOptions; +import com.epam.deltix.timebase.messages.service.FeedStatus; +import com.epam.deltix.timebase.messages.service.SecurityFeedStatusMessage; import com.epam.deltix.timebase.messages.universal.*; +import com.epam.deltix.util.annotations.Alphanumeric; import com.epam.deltix.util.collections.generated.ObjectList; /** @@ -27,26 +33,23 @@ */ class L1SingleExchangeQuoteProcessor implements L1Processor { + private final ObjectPool pool; + protected final L1MarketSide bids; protected final L1MarketSide asks; - private final MutableExchangeList>> exchanges; - // Parameters - private final UpdateMode updateMode; - private final ObjectPool pool; + private final EventHandler eventHandler; - /** - * This parameter using for handle book reset entry. - * - * @see QuoteProcessor#isWaitingForSnapshot() - */ - private boolean isWaitingForSnapshot = false; + // Parameters + private final DisconnectMode disconnectMode; - L1SingleExchangeQuoteProcessor(final ObjectPool pool, - final UpdateMode updateMode) { + L1SingleExchangeQuoteProcessor(final OrderBookOptions options, + final ObjectPool pool) { this.pool = pool; - this.updateMode = updateMode; + this.disconnectMode = options.getDisconnectMode().orElse(Defaults.DISCONNECT_MODE); + this.eventHandler = new EventHandlerImpl(options); + this.asks = L1MarketSide.factory(QuoteSide.ASK); this.bids = L1MarketSide.factory(QuoteSide.BID); this.exchanges = new MutableExchangeListImpl<>(); @@ -74,8 +77,8 @@ public boolean isEmpty() { } @Override - public Quote processL1EntryNewInfo(final L1EntryInfo l1EntryNewInfo) { - final long exchangeId = l1EntryNewInfo.getExchangeId(); + public Quote processL1EntryNew(final PackageHeaderInfo pck, final L1EntryInfo msg) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); final Option>> exchange = getOrCreateExchange(exchangeId); if (!exchange.hasValue()) { // TODO add null check @@ -86,16 +89,7 @@ public Quote processL1EntryNewInfo(final L1EntryInfo l1EntryNewInfo) { return null; } - if (exchange.get().getProcessor().isEmpty()) { - switch (updateMode) { - case WAITING_FOR_SNAPSHOT: - return null; // Todo ADD null check!! - case NON_WAITING_FOR_SNAPSHOT: - break; - } - } - - final QuoteSide side = l1EntryNewInfo.getSide(); + final QuoteSide side = msg.getSide(); final L1MarketSide marketSide = exchange.get().getProcessor().getMarketSide(side); final Quote quote; @@ -105,24 +99,29 @@ public Quote processL1EntryNewInfo(final L1EntryInfo l1EntryNewInfo) { } else { quote = marketSide.getBestQuote(); } - updateByL1EntryNew(quote, l1EntryNewInfo); + quote.copyFrom(pck, msg); return quote; } @Override // TODO add validation for exchange id - public void processL1VendorSnapshot(final PackageHeaderInfo marketMessageInfo) { - final ObjectList entries = marketMessageInfo.getEntries(); + public boolean processL1Snapshot(final PackageHeaderInfo pck) { + if (!eventHandler.isSnapshotAllowed(pck.getPackageType())) { + return false; + } + + // We expect that all entries are sorted by exchange id + final ObjectList entries = pck.getEntries(); for (int i = 0; i < entries.size(); i++) { - final BaseEntryInfo pck = entries.get(i); - final L1EntryInfo l1EntryInfo = (L1EntryInfo) pck; - final QuoteSide side = l1EntryInfo.getSide(); - final long exchangeId = l1EntryInfo.getExchangeId(); + final BaseEntryInfo entryInfo = entries.get(i); + final L1EntryInfo entry = (L1EntryInfo) entryInfo; + final QuoteSide side = entry.getSide(); + @Alphanumeric final long exchangeId = entry.getExchangeId(); final Option>> exchange = getOrCreateExchange(exchangeId); if (!exchange.hasValue()) { // TODO Log error and throw exception or add package validation - continue; + return false; } final L1MarketSide marketSide = exchange.get().getProcessor().getMarketSide(side); @@ -134,33 +133,46 @@ public void processL1VendorSnapshot(final PackageHeaderInfo marketMessageInfo) { } else { quote = marketSide.getBestQuote(); } - updateByL1EntryNew(quote, l1EntryInfo); + quote.copyFrom(pck, entry); } - notWaitingForSnapshot(); + eventHandler.onSnapshot(); + return true; } @Override public boolean isWaitingForSnapshot() { - return isWaitingForSnapshot; + return eventHandler.isWaitingForSnapshot(); } - private void waitingForSnapshot() { - if (!isWaitingForSnapshot()) { - isWaitingForSnapshot = true; - } - } + @Override + public boolean processBookResetEntry(final PackageHeaderInfo pck, final BookResetEntryInfo msg) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final Option>> exchange = getOrCreateExchange(exchangeId); - private void notWaitingForSnapshot() { - if (isWaitingForSnapshot()) { - isWaitingForSnapshot = false; + if (exchange.hasValue()) { + clear(); + eventHandler.onReset(); + return true; + } else { + return false; } } @Override - public void processBookResetEntry(final BookResetEntryInfo bookResetEntryInfo) { - clear(); - waitingForSnapshot(); + public boolean processSecurityFeedStatus(final SecurityFeedStatusMessage msg) { + if (msg.getStatus() == FeedStatus.NOT_AVAILABLE) { + if (disconnectMode == DisconnectMode.CLEAR_EXCHANGE) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final Option>> exchange = getOrCreateExchange(exchangeId); + if (exchange.hasValue()) { + clear(); + eventHandler.onDisconnect(); + return true; + } + } + } + return false; } @Override @@ -168,12 +180,12 @@ public ExchangeList>> getExchanges() { return exchanges; } - private void releaseAndClean(final L1MarketSide marketSide) { - for (int i = 0; i < marketSide.depth(); i++) { - final Quote quote = marketSide.getQuote(i); + private void releaseAndClean(final L1MarketSide side) { + for (int i = 0; i < side.depth(); i++) { + final Quote quote = side.getQuote(i); pool.release(quote); } - marketSide.clear(); + side.clear(); } /** @@ -183,33 +195,13 @@ private void releaseAndClean(final L1MarketSide marketSide) { * @param exchangeId - id of exchange. * @return exchange book by id. */ - private Option>> getOrCreateExchange(final long exchangeId) { + private Option>> getOrCreateExchange(@Alphanumeric final long exchangeId) { if (!exchanges.isEmpty()) { return exchanges.getById(exchangeId); } - final MutableExchangeImpl> exchange = new MutableExchangeImpl<>(exchangeId, this); + final MutableExchange> exchange = new MutableExchangeImpl<>(exchangeId, this); exchanges.add(exchange); return exchanges.getById(exchangeId); } - /** - * Update quote with L1EntryNew. - * - * @param l1EntryInfo - L1EntryNew - * @param quote - type of quote - */ - protected void updateByL1EntryNew(final Quote quote, final L1EntryInfo l1EntryInfo) { - if (quote.getSize() != l1EntryInfo.getSize()) { - quote.setSize(l1EntryInfo.getSize()); - } - if (quote.getPrice() != l1EntryInfo.getPrice()) { - quote.setPrice(l1EntryInfo.getPrice()); - } - if (quote.getExchangeId() != l1EntryInfo.getExchangeId()) { - quote.setExchangeId(l1EntryInfo.getExchangeId()); - } - if (quote.getNumberOfOrders() != l1EntryInfo.getNumberOfOrders()) { - quote.setNumberOfOrders(l1EntryInfo.getNumberOfOrders()); - } - } } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2AggregatedQuoteProcessor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2AggregatedQuoteProcessor.java index 983f983..bf76e9d 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2AggregatedQuoteProcessor.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2AggregatedQuoteProcessor.java @@ -16,17 +16,15 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.dfp.Decimal; -import com.epam.deltix.orderbook.core.options.GapMode; -import com.epam.deltix.orderbook.core.options.UnreachableDepthMode; -import com.epam.deltix.orderbook.core.options.UpdateMode; +import com.epam.deltix.orderbook.core.options.OrderBookOptions; import com.epam.deltix.timebase.messages.TypeConstants; import com.epam.deltix.timebase.messages.universal.L2EntryUpdateInfo; import com.epam.deltix.timebase.messages.universal.QuoteSide; import static com.epam.deltix.dfp.Decimal64Utils.*; - /** * Implementation aggregated order book for L2 quote level. * @@ -34,14 +32,8 @@ */ class L2AggregatedQuoteProcessor extends AbstractL2MultiExchangeProcessor { - L2AggregatedQuoteProcessor(final int initialExchangeCount, - final int initialDepth, - final int maxDepth, - final ObjectPool pool, - final GapMode gapMode, - final UpdateMode updateMode, - final UnreachableDepthMode unreachableDepthMode) { - super(initialExchangeCount, initialDepth, maxDepth, pool, gapMode, updateMode, unreachableDepthMode); + L2AggregatedQuoteProcessor(final OrderBookOptions options, final ObjectPool pool) { + super(options, pool); } @Override @@ -50,11 +42,9 @@ public String getDescription() { } @Override - public void updateQuote(final Quote previous, - final QuoteSide side, - final L2EntryUpdateInfo update) { + public void updateQuote(final Quote previous, final QuoteSide side, final L2EntryUpdateInfo update) { final L2MarketSide marketSide = getMarketSide(side); - final short level = marketSide.binarySearchLevelByPrice(previous); + final int level = marketSide.binarySearch(previous); if (level != L2MarketSide.NOT_FOUND) { final Quote quote = marketSide.getQuote(level); @Decimal final long size = add(subtract(quote.getSize(), previous.getSize()), update.getSize()); @@ -66,9 +56,8 @@ public void updateQuote(final Quote previous, } @Override - public boolean removeQuote(final Quote remove, - final L2MarketSide marketSide) { - final short level = marketSide.binarySearchLevelByPrice(remove); + public boolean removeQuote(final Quote remove, final L2MarketSide marketSide) { + final int level = marketSide.binarySearch(remove); if (level != L2MarketSide.NOT_FOUND) { final Quote quote = marketSide.getQuote(level); @@ -89,7 +78,7 @@ public boolean removeQuote(final Quote remove, @Override public Quote insertQuote(final Quote insert, final L2MarketSide marketSide) { - final short level = marketSide.binarySearchNextLevelByPrice(insert); + final int level = marketSide.binarySearchNextLevelByPrice(insert); Quote quote; if (level != marketSide.depth()) { quote = marketSide.getQuote(level); @@ -128,7 +117,7 @@ public void clear() { } @Override - public L2Processor clearExchange(final L2Processor exchange) { + public L2Processor unmapQuote(final L2Processor exchange) { if (exchange.isEmpty()) { return exchange; } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2ConsolidatedQuoteProcessor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2ConsolidatedQuoteProcessor.java index b3d179d..e1b868c 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2ConsolidatedQuoteProcessor.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2ConsolidatedQuoteProcessor.java @@ -16,10 +16,9 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.dfp.Decimal64Utils; -import com.epam.deltix.orderbook.core.options.GapMode; -import com.epam.deltix.orderbook.core.options.UnreachableDepthMode; -import com.epam.deltix.orderbook.core.options.UpdateMode; +import com.epam.deltix.orderbook.core.options.OrderBookOptions; import com.epam.deltix.timebase.messages.universal.L2EntryUpdateInfo; import com.epam.deltix.timebase.messages.universal.QuoteSide; @@ -30,14 +29,8 @@ */ class L2ConsolidatedQuoteProcessor extends AbstractL2MultiExchangeProcessor { - L2ConsolidatedQuoteProcessor(final int initialExchangeCount, - final int initialDepth, - final int maxDepth, - final ObjectPool pool, - final GapMode gapMode, - final UpdateMode updateMode, - final UnreachableDepthMode unreachableDepthMode) { - super(initialExchangeCount, initialDepth, maxDepth, pool, gapMode, updateMode,unreachableDepthMode); + L2ConsolidatedQuoteProcessor(final OrderBookOptions options, final ObjectPool pool) { + super(options, pool); } @Override @@ -61,28 +54,33 @@ public void clear() { @Override public boolean removeQuote(final Quote remove, final L2MarketSide marketSide) { - final short level = marketSide.binarySearchLevelByPrice(remove); + final int level = marketSide.binarySearch(remove); if (level != L2MarketSide.NOT_FOUND) { - if (remove.getExchangeId() == marketSide.getQuote(level).getExchangeId() && - Decimal64Utils.equals(remove.getPrice(), marketSide.getQuote(level).getPrice())) { + if (remove.equals(marketSide.getQuote(level))) { marketSide.remove(level); return true; } else { - final int size = exchanges.size(); - for (int i = 0, k = level + i; i < size; i++, k = level + i) { - if (marketSide.hasLevel((short) (k))) { - if (remove.getExchangeId() == marketSide.getQuote(k).getExchangeId() && - Decimal64Utils.equals(remove.getPrice(), marketSide.getQuote(k).getPrice())) { + final int depth = marketSide.depth(); + for (int i = 0, k = level + i; i < depth; i++, k = level + i) { + if (marketSide.hasLevel(k)) { + final Quote quote = marketSide.getQuote(k); + if (Decimal64Utils.isNotEqual(remove.getPrice(), quote.getPrice())) { + break; + } + if (remove.equals(quote)) { marketSide.remove(k); return true; } } } - for (int i = 0, k = level - i; i < size; i++, k = level - i) { - if (marketSide.hasLevel((short) (k))) { - if (remove.getExchangeId() == marketSide.getQuote(k).getExchangeId() && - Decimal64Utils.equals(remove.getPrice(), marketSide.getQuote(k).getPrice())) { + for (int i = 0, k = level - i; i < depth; i++, k = level - i) { + if (marketSide.hasLevel(k)) { + final Quote quote = marketSide.getQuote(k); + if (Decimal64Utils.isNotEqual(remove.getPrice(), quote.getPrice())) { + break; + } + if (remove.equals(quote)) { marketSide.remove(k); return true; } @@ -95,13 +93,13 @@ public boolean removeQuote(final Quote remove, final L2MarketSide marketS @Override public Quote insertQuote(final Quote insert, final L2MarketSide marketSide) { - final short level = marketSide.binarySearchNextLevelByPrice(insert); + final int level = marketSide.binarySearchNextLevelByPrice(insert); marketSide.add(level, insert); return insert; } @Override - public L2Processor clearExchange(final L2Processor exchange) { + public L2Processor unmapQuote(final L2Processor exchange) { removeAll(exchange, QuoteSide.ASK); removeAll(exchange, QuoteSide.BID); exchange.clear(); diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2MarketSide.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2MarketSide.java index 1b0f807..83cb52d 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2MarketSide.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2MarketSide.java @@ -16,8 +16,11 @@ */ package com.epam.deltix.orderbook.core.impl; +import com.epam.deltix.dfp.Decimal; import com.epam.deltix.orderbook.core.api.MarketSide; +import com.epam.deltix.timebase.messages.universal.BookUpdateAction; import com.epam.deltix.timebase.messages.universal.QuoteSide; +import com.epam.deltix.util.annotations.Alphanumeric; import java.util.Objects; @@ -34,55 +37,46 @@ static L2MarketSide factory(final i Objects.requireNonNull(side); switch (side) { case BID: - return new AbstractL2MarketSide.BID<>(initialDepth, (short) maxDepth); + return new AbstractL2MarketSide.BID<>(initialDepth, maxDepth); case ASK: - return new AbstractL2MarketSide.ASK<>(initialDepth, (short) maxDepth); + return new AbstractL2MarketSide.ASK<>(initialDepth, maxDepth); default: throw new IllegalStateException("Unexpected value: " + side); } } + @Override + default Quote getQuote(final CharSequence quoteId) { + // Not supported for L2 + return null; + } + + @Override + default boolean hasQuote(final CharSequence quoteId) { + // Not supported for L2 + return false; + } + /** * Inserts the specified quote at the specified level. Shifts the quotes right (adds one to their indices). * * @param level level to be inserted * @param insert quote to be inserted */ - void add(short level, Quote insert); + void add(int level, Quote insert); /** * Inserts the specified quote at the end. Shifts the quotes right (adds one to their indices). * * @param insert quote to be inserted */ - void addLast(Quote insert); + void addWorstQuote(Quote insert); - /** - * Inserts the specified quote by price. Shifts the quotes right (adds one to their indices). - * - * @param insert quote to be inserted - */ - void add(Quote insert); - - short getMaxDepth(); - - short binarySearchLevelByPrice(Quote find); + int getMaxDepth(); - short binarySearchNextLevelByPrice(Quote find); - - /** - * Trims the limit depth of market. - * An application can use this operation to minimize the storage of stock quotes. - * After trim, we can't add stock quote with level more than limit. - */ - void trim(); + int binarySearch(Quote find); - /** - * Return worst quote. - * - * @return last quote - */ - Quote getWorstQuote(); + int binarySearchNextLevelByPrice(Quote find); /** * Remove worst quote. @@ -93,6 +87,7 @@ static L2MarketSide factory(final i /** * Remove quote by level. + * Shifts any subsequent elements to the left. * * @param level - the level of the quote to be removed * @return removed quote @@ -112,8 +107,37 @@ static L2MarketSide factory(final i * @param level - quote level to use * @return true if this quote level is unexpected */ - boolean isGap(final short level); + boolean isGap(int level); + + + boolean isUnreachableLeve(int level); + + /** + * Checks if the specified price is sorted. + * + * @param level - quote level to use + * @param price - price to be checked + * @return true if this price is sorted. + */ + boolean checkOrderPrice(int level, @Decimal long price); void clear(); + /** + * Validates state of market side. + * + * @return - true if state is valid + */ + boolean validateState(); + + boolean isInvalidInsert(int level, + @Decimal long price, + @Decimal long size, + @Alphanumeric long exchangeId); + + boolean isInvalidUpdate(BookUpdateAction action, + int level, + @Decimal long price, + @Decimal long size, + @Alphanumeric long exchangeId); } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2OrderBookFactory.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2OrderBookFactory.java index 623a958..70efb69 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2OrderBookFactory.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2OrderBookFactory.java @@ -16,12 +16,11 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.orderbook.core.api.OrderBook; import com.epam.deltix.orderbook.core.api.OrderBookQuote; -import com.epam.deltix.orderbook.core.options.GapMode; -import com.epam.deltix.orderbook.core.options.Option; -import com.epam.deltix.orderbook.core.options.UnreachableDepthMode; -import com.epam.deltix.orderbook.core.options.UpdateMode; +import com.epam.deltix.orderbook.core.options.Defaults; +import com.epam.deltix.orderbook.core.options.OrderBookOptions; /** * A factory that implements order book for Level2. @@ -34,79 +33,73 @@ @SuppressWarnings("unchecked") public class L2OrderBookFactory { + /** + * Prevents instantiation + */ + protected L2OrderBookFactory() { + } + /** * Creates OrderBook for single exchange market feed of given initial depth * - * @param symbol - type of symbol - * @param initialDepth - initial book depth - * @param maxDepth - max order book depth - * @param gapMode - skipped levels mode - * @param updateMode - modes of order book update. - * @param - type of quote + * @param options - options to use + * @param - type of quote * @return instance of OrderBook for single exchange */ - public static OrderBook newSingleExchangeBook(final Option symbol, - final int initialDepth, - final int maxDepth, - final GapMode gapMode, - final UpdateMode updateMode, - final UnreachableDepthMode unreachableDepthMode) { - final ObjectPool pool = new ObjectPool<>(initialDepth, MutableOrderBookQuoteImpl::new); - final QuoteProcessor processor = - new L2SingleExchangeQuoteProcessor<>(initialDepth, maxDepth, pool, gapMode, updateMode, unreachableDepthMode); - return (OrderBook) new OrderBookDecorator<>(symbol, processor); + public static OrderBook newSingleExchangeBook(final OrderBookOptions options) { + final int maxDepth = options.getMaxDepth().orElse(Defaults.MAX_DEPTH); + final int depth = options.getInitialDepth().orElse(Math.min(Defaults.INITIAL_DEPTH, maxDepth)); + final boolean isCompact = options.isCompactVersion().orElse(false); + + final QuoteProcessor processor; + if (isCompact) { + processor = new CompactL2SingleExchangeQuoteProcessor<>(options); + } else { + final ObjectPool pool = + (ObjectPool) options.getSharedObjectPool().orElse(QuotePoolFactory.create(options, depth)); + processor = new L2SingleExchangeQuoteProcessor<>(options, pool); + } + + return (OrderBook) new OrderBookDecorator<>(options.getSymbol(), processor); } /** * Creates OrderBook for market feed from multiple exchanges of given maximum depth. * Consolidated book preserve information about quote's exchange. * - * @param symbol - type of symbol\ - * @param initialExchangeCount - initial pool size for stock exchanges - * @param initialDepth - initial book depth - * @param maxDepth - max order book depth - * @param gapMode - skipped levels mode - * @param updateMode - modes of order book update. - * @param - type of quote + * @param options - options to use + * @param - type of quote * @return instance of Order Book with multiple exchanges */ - public static OrderBook newConsolidatedBook(final Option symbol, - final int initialExchangeCount, - final int initialDepth, - final int maxDepth, - final GapMode gapMode, - final UpdateMode updateMode, - final UnreachableDepthMode unreachableDepthMode) { - final ObjectPool pool = new ObjectPool<>(initialExchangeCount * initialDepth, MutableOrderBookQuoteImpl::new); - final QuoteProcessor processor = - new L2ConsolidatedQuoteProcessor<>(initialExchangeCount, initialDepth, maxDepth, pool, gapMode, updateMode, unreachableDepthMode); - return (OrderBook) new OrderBookDecorator<>(symbol, processor); + public static OrderBook newConsolidatedBook(final OrderBookOptions options) { + final int maxDepth = options.getMaxDepth().orElse(Defaults.MAX_DEPTH); + final int depth = options.getInitialDepth().orElse(Math.min(Defaults.INITIAL_DEPTH, maxDepth)); + final int exchanges = options.getInitialExchangesPoolSize().orElse(Defaults.INITIAL_EXCHANGES_POOL_SIZE); + + final ObjectPool pool = (ObjectPool) options.getSharedObjectPool() + .orElse(QuotePoolFactory.create(options, exchanges * depth)); + + final QuoteProcessor processor = new L2ConsolidatedQuoteProcessor<>(options, pool); + return (OrderBook) new OrderBookDecorator<>(options.getSymbol(), processor); } /** * Creates OrderBook for market feed from multiple exchanges of given maximum depth. * Aggregated order book groups quotes from multiple exchanges by price. * - * @param symbol - type of symbol\ - * @param initialExchangeCount - initial pool size for stock exchanges - * @param initialDepth - initial book depth - * @param maxDepth - max order book depth - * @param gapMode - skipped levels mode - * @param updateMode - modes of order book update. - * @param - type of quote + * @param options - options to use + * @param - type of quote * @return instance of Order Book with multiple exchanges */ - public static OrderBook newAggregatedBook(final Option symbol, - final int initialExchangeCount, - final int initialDepth, - final int maxDepth, - final GapMode gapMode, - final UpdateMode updateMode, - final UnreachableDepthMode unreachableDepthMode) { - final ObjectPool pool = new ObjectPool<>(initialExchangeCount * initialDepth * 4, MutableOrderBookQuoteImpl::new); - final QuoteProcessor processor = - new L2AggregatedQuoteProcessor<>(initialExchangeCount, initialDepth, maxDepth, pool, gapMode, updateMode, unreachableDepthMode); - return (OrderBook) new OrderBookDecorator<>(symbol, processor); - } + public static OrderBook newAggregatedBook(final OrderBookOptions options) { + final int maxDepth = options.getMaxDepth().orElse(Defaults.MAX_DEPTH); + final int depth = options.getInitialDepth().orElse(Math.min(Defaults.INITIAL_DEPTH, maxDepth)); + final int exchanges = options.getInitialExchangesPoolSize().orElse(Defaults.INITIAL_EXCHANGES_POOL_SIZE); + + final ObjectPool pool = (ObjectPool) options.getSharedObjectPool() + .orElse(QuotePoolFactory.create(options, exchanges * depth * 4)); + final QuoteProcessor processor = new L2AggregatedQuoteProcessor<>(options, pool); + return (OrderBook) new OrderBookDecorator<>(options.getSymbol(), processor); + } } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2Processor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2Processor.java index c364646..0487c05 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2Processor.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2Processor.java @@ -16,6 +16,7 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.timebase.messages.universal.*; import com.epam.deltix.util.collections.generated.ObjectList; @@ -24,7 +25,7 @@ * * @author Andrii_Ostapenko */ -interface L2Processor extends QuoteProcessor, ResetEntryProcessor { +interface L2Processor extends QuoteProcessor { @Override default DataModelType getQuoteLevels() { @@ -32,54 +33,64 @@ default DataModelType getQuoteLevels() { } @Override - L2MarketSide getMarketSide(final QuoteSide side); + L2MarketSide getMarketSide(QuoteSide side); @Override MutableExchangeList>> getExchanges(); /** - * This type reports incremental Level2-new: insert one line in Order Book either on ask or bid side. + * This type reports incremental Level2-new: insert one line in Order Book either on ask or bid side. * - * @param l2EntryNewInfo - Level2-new entry + * @param pck + * @param msg - Level2-new entry * @return insert quote */ - Quote processL2EntryNewInfo(final L2EntryNewInfo l2EntryNewInfo); + Quote processL2EntryNew(PackageHeaderInfo pck, L2EntryNewInfo msg); /** * This type reports incremental Level2-update: update or delete one line in Order Book either on ask or bid side. * - * @param l2EntryUpdateInfo - Level2-update + * @param pck + * @param msg - Level2-update + * @return true if quote was updated, false if quote was not found */ - void processL2EntryUpdateInfo(final L2EntryUpdateInfo l2EntryUpdateInfo); + boolean processL2EntryUpdate(PackageHeaderInfo pck, L2EntryUpdateInfo msg); - void processL2VendorSnapshot(final PackageHeaderInfo marketMessageInfo); + boolean processL2Snapshot(PackageHeaderInfo msg); - default boolean process(final BaseEntryInfo pck) { - if (pck instanceof L2EntryNew) { - final L2EntryNew l2EntryNewInfo = (L2EntryNew) pck; - processL2EntryNewInfo(l2EntryNewInfo); - return true; - } else if (pck instanceof L2EntryUpdate) { - final L2EntryUpdate l2EntryUpdateInfo = (L2EntryUpdate) pck; - processL2EntryUpdateInfo(l2EntryUpdateInfo); + boolean isWaitingForSnapshot(); + + boolean isSnapshotAllowed(PackageHeaderInfo msg); + + default boolean processIncrementalUpdate(PackageHeaderInfo pck, final BaseEntryInfo entryInfo) { + if (entryInfo instanceof L2EntryNew) { + final L2EntryNew entry = (L2EntryNew) entryInfo; + return processL2EntryNew(pck, entry) != null; + } else if (entryInfo instanceof L2EntryUpdate) { + final L2EntryUpdate entry = (L2EntryUpdate) entryInfo; + return processL2EntryUpdate(pck, entry); + } else if (entryInfo instanceof StatisticsEntry) { return true; } return false; } - default boolean processSnapshot(final PackageHeaderInfo marketMessageInfo) { - final ObjectList entries = marketMessageInfo.getEntries(); - final BaseEntryInfo baseEntryInfo = entries.get(0); - if (baseEntryInfo instanceof L2EntryNewInfo) { - processL2VendorSnapshot(marketMessageInfo); - return true; - } else if (baseEntryInfo instanceof BookResetEntryInfo) { - final BookResetEntryInfo resetEntryInfo = (BookResetEntryInfo) baseEntryInfo; - if (resetEntryInfo.getModelType() == getQuoteLevels()) { - processBookResetEntry(resetEntryInfo); - return true; + default boolean processSnapshot(final PackageHeaderInfo msg) { + final ObjectList entries = msg.getEntries(); + final int n = entries.size(); + // skip statistic entries try to establish if we are dealing with order book reset or normal snapshot + for (int i = 0; i < n; i++) { + final BaseEntryInfo entry = entries.get(i); + if (entry instanceof L2EntryNewInfo) { + return processL2Snapshot(msg); + } else if (entry instanceof BookResetEntryInfo) { + final BookResetEntryInfo resetEntry = (BookResetEntryInfo) entry; + if (resetEntry.getModelType() == getQuoteLevels()) { + return processBookResetEntry(msg, resetEntry); + } } } return false; } + } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2SingleExchangeQuoteProcessor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2SingleExchangeQuoteProcessor.java index df18ec3..9d9fc3b 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2SingleExchangeQuoteProcessor.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L2SingleExchangeQuoteProcessor.java @@ -16,16 +16,19 @@ */ package com.epam.deltix.orderbook.core.impl; -import com.epam.deltix.orderbook.core.options.GapMode; -import com.epam.deltix.orderbook.core.options.Option; -import com.epam.deltix.orderbook.core.options.UnreachableDepthMode; -import com.epam.deltix.orderbook.core.options.UpdateMode; +import com.epam.deltix.containers.AlphanumericUtils; +import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.timebase.messages.TypeConstants; +import com.epam.deltix.timebase.messages.service.FeedStatus; +import com.epam.deltix.timebase.messages.service.SecurityFeedStatusMessage; import com.epam.deltix.timebase.messages.universal.*; +import com.epam.deltix.util.annotations.Alphanumeric; import com.epam.deltix.util.collections.generated.ObjectList; import static com.epam.deltix.timebase.messages.universal.QuoteSide.ASK; import static com.epam.deltix.timebase.messages.universal.QuoteSide.BID; + /** * @author Andrii_Ostapenko1 */ @@ -35,45 +38,32 @@ public class L2SingleExchangeQuoteProcessor protected final L2MarketSide bids; protected final L2MarketSide asks; - private final MutableExchangeList>> exchanges; + private final EventHandler eventHandler; + //Parameters - private final GapMode gapMode; + private final ValidationOptions validationOptions; + private final DisconnectMode disconnectMode; - private final UnreachableDepthMode unreachableDepthMode; - private final UpdateMode updateMode; + public L2SingleExchangeQuoteProcessor(final OrderBookOptions options, final ObjectPool pool) { + this.disconnectMode = options.getDisconnectMode().orElse(Defaults.DISCONNECT_MODE); + this.validationOptions = options.getInvalidQuoteMode().orElse(Defaults.VALIDATION_OPTIONS); + this.eventHandler = new EventHandlerImpl(options); - /** - * This parameter using for handle book reset entry. - * - * @see QuoteProcessor#isWaitingForSnapshot() - */ - private boolean isWaitingForSnapshot = false; + this.pool = pool; + this.exchanges = new MutableExchangeListImpl<>(); - public L2SingleExchangeQuoteProcessor(final int initialDepth, - final int maxDepth, - final ObjectPool pool, - final GapMode gapMode, - final UpdateMode updateMode, - final UnreachableDepthMode unreachableDepthMode) { - this.unreachableDepthMode = unreachableDepthMode; + final int maxDepth = options.getMaxDepth().orElse(Defaults.MAX_DEPTH); + final int initialDepth = options.getInitialDepth().orElse(Math.min(Defaults.INITIAL_DEPTH, maxDepth)); this.asks = L2MarketSide.factory(initialDepth, maxDepth, ASK); this.bids = L2MarketSide.factory(initialDepth, maxDepth, BID); - this.pool = pool; - this.gapMode = gapMode; - this.updateMode = updateMode; - this.exchanges = new MutableExchangeListImpl<>(); } - public L2SingleExchangeQuoteProcessor(final long exchangeId, - final int initialDepth, - final int maxDepth, + public L2SingleExchangeQuoteProcessor(final OrderBookOptions options, final ObjectPool pool, - final GapMode gapMode, - final UpdateMode updateMode, - final UnreachableDepthMode unreachableDepthMode) { - this(initialDepth, maxDepth, pool, gapMode, updateMode, unreachableDepthMode); + @Alphanumeric final long exchangeId) { + this(options, pool); getOrCreateExchange(exchangeId); } @@ -83,8 +73,8 @@ public String getDescription() { } @Override - public Quote processL2EntryNewInfo(final L2EntryNewInfo l2EntryNewInfo) { - final long exchangeId = l2EntryNewInfo.getExchangeId(); + public Quote processL2EntryNew(final PackageHeaderInfo pck, final L2EntryNewInfo msg) { + final long exchangeId = msg.getExchangeId(); final Option>> exchange = getOrCreateExchange(exchangeId); if (!exchange.hasValue()) { // TODO Log warning!! @@ -92,152 +82,196 @@ public Quote processL2EntryNewInfo(final L2EntryNewInfo l2EntryNewInfo) { return null; } - if (exchange.get().getProcessor().isEmpty()) { - switch (updateMode) { - case WAITING_FOR_SNAPSHOT: - return null; // Todo ADD null check!! - case NON_WAITING_FOR_SNAPSHOT: - break; - } + if (exchange.get().getProcessor().isWaitingForSnapshot()) { + return null; } - final QuoteSide side = l2EntryNewInfo.getSide(); + final QuoteSide side = msg.getSide(); final L2MarketSide marketSide = exchange.get().getProcessor().getMarketSide(side); - final short level = l2EntryNewInfo.getLevel(); - - if (level >= marketSide.getMaxDepth()) { - switch (unreachableDepthMode) { - case SKIP_AND_DROP: - clear(); - return null; - case SKIP: - default: - return null; - } - // Unreachable quote level - } + final int level = msg.getLevel(); - // TODO: 6/30/2022 need to refactor return value - if (marketSide.isGap(level)) { - switch (gapMode) { - case FILL_GAP: - checkAndFillGap(l2EntryNewInfo); - return marketSide.getWorstQuote(); - case SKIP_AND_DROP: - clear(); - return null; - case SKIP: - default: - return null; + if (marketSide.isInvalidInsert(level, msg.getPrice(), msg.getSize(), exchangeId)) { + if (validationOptions.isQuoteInsert()) { + clear(); + eventHandler.onBroken(); } + return null; } final Quote quote; - if (level == marketSide.depth()) {// Add new worst quote + if (level == marketSide.depth()) { // Add new worst quote quote = pool.borrow(); } else if (marketSide.isFull()) { // Check side is Full and remove Worst quote quote = marketSide.removeWorstQuote(); - // Attention we remove worst quote bat not remove quote from multi exchange } else { quote = pool.borrow(); } - mapNewL2ToQuote(l2EntryNewInfo, quote); + quote.copyFrom(pck, msg); marketSide.add(level, quote); - -// System.out.printf("Insert: Quote %s to exchange by level %s \n", quote, level);// Add logger - return quote; } @Override - public void processL2EntryUpdateInfo(final L2EntryUpdateInfo l2EntryUpdateInfo) { - final long exchangeId = l2EntryUpdateInfo.getExchangeId(); - final Option>> exchange = getOrCreateExchange(exchangeId); + public boolean processL2EntryUpdate(final PackageHeaderInfo pck, final L2EntryUpdateInfo msg) { + final QuoteSide side = msg.getSide(); + final int level = msg.getLevel(); + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final BookUpdateAction action = msg.getAction(); + + final Option>> exchange = getExchanges().getById(exchangeId); if (!exchange.hasValue()) { // TODO move to another palace - return; + return false; } - final QuoteSide side = l2EntryUpdateInfo.getSide(); - final short level = l2EntryUpdateInfo.getLevel(); + if (exchange.get().getProcessor().isWaitingForSnapshot()) { + return false; + } final L2MarketSide marketSide = exchange.get().getProcessor().getMarketSide(side); - - if (!marketSide.hasLevel(level)) { - return; + if (marketSide.isInvalidUpdate(action, level, msg.getPrice(), msg.getSize(), exchangeId)) { + if (validationOptions.isQuoteUpdate()) { + clear(); + eventHandler.onBroken(); + return false; + } + return true; // skip invalid update } - final BookUpdateAction bookUpdateAction = l2EntryUpdateInfo.getAction(); - if (bookUpdateAction == BookUpdateAction.DELETE) { + if (action == BookUpdateAction.DELETE) { final Quote remove = marketSide.remove(level); pool.release(remove); -// System.out.printf("Remove: Quote %s from exchange by level %s\n", remove, level); // TODO add logger - } else if (bookUpdateAction == BookUpdateAction.UPDATE) { + } else if (action == BookUpdateAction.UPDATE) { final Quote quote = marketSide.getQuote(level); - mapUpdateL2ToQuote(l2EntryUpdateInfo, quote); -// System.out.printf("Update: Quote %s from exchange by level %s\n", quote, level); // TODO add logger + quote.copyFrom(pck, msg); } + return true; } @Override - // TODO add validation for exchange id - public void processL2VendorSnapshot(final PackageHeaderInfo marketMessageInfo) { - final ObjectList entries = marketMessageInfo.getEntries(); - if (entries.size() < asks.depth() + bids.depth()) { - clear(); + public boolean processL2Snapshot(final PackageHeaderInfo pck) { + if (!isSnapshotAllowed(pck)) { + return false; } + final ObjectList entries = pck.getEntries(); + + final int prevAsksDepth = asks.depth(); + final int prevBidsDepth = bids.depth(); + int askCnt = 0; int bidCnt = 0; for (int i = 0; i < entries.size(); i++) { - final BaseEntryInfo pck = entries.get(i); - final L2EntryNewInfo l2EntryNewInfo = (L2EntryNew) pck; - final short level = l2EntryNewInfo.getLevel(); - final QuoteSide side = l2EntryNewInfo.getSide(); - final long exchangeId = l2EntryNewInfo.getExchangeId(); - - final Option>> exchange = getOrCreateExchange(exchangeId); - if (!exchange.hasValue()) { - // TODO LOG warning - continue; + final BaseEntryInfo e = entries.get(i); + if (e instanceof L2EntryNewInterface) { + final L2EntryNewInterface entry = (L2EntryNewInterface) e; + +// //We expect that all entries are sorted by side and level +// if (entry == null || entry.getSide() == null || +// (entry.getSide() == ASK && askCnt != entry.getLevel()) || +// (entry.getSide() == BID && bidCnt != entry.getLevel())) { +// clear(); +// eventHandler.onBroken(); +// return false; +// } + + final int level = entry.getLevel(); + final QuoteSide side = entry.getSide(); + @Alphanumeric final long exchangeId = entry.getExchangeId(); + + // We expect that exchangeId is valid and all entries have the same exchangeId + final Option>> exchange = getOrCreateExchange(exchangeId); + if (!exchange.hasValue()) { + clear(); + eventHandler.onBroken(); + return false; + } + + final L2MarketSide marketSide = exchange.get().getProcessor().getMarketSide(side); + + // Both side have the same max depth + final int maxDepth = marketSide.getMaxDepth(); + if ((side == ASK && askCnt == maxDepth) || (side == BID && bidCnt == maxDepth)) { + continue; + } + + if (marketSide.hasLevel(level)) { + final Quote quote = marketSide.getQuote(level); + quote.copyFrom(pck, entry); + } else { + final Quote quote = pool.borrow(); + quote.copyFrom(pck, entry); + marketSide.add(level, quote); + } + + if (side == ASK) { + askCnt++; + } else { + bidCnt++; + } + + if (askCnt == maxDepth && bidCnt == maxDepth) { + break; + } } + } - final L2MarketSide marketSide = exchange.get().getProcessor().getMarketSide(side); + //Remove all worst quotes after snapshot. + //We're doing this because we don't release quotes during snapshot processing. + for (int i = askCnt; i < prevAsksDepth; i++) { + final Quote quote = asks.removeWorstQuote(); + pool.release(quote); + } - // Both side have the same max depth - final short maxDepth = marketSide.getMaxDepth(); - if ((side == ASK && askCnt == maxDepth) || (side == BID && bidCnt == maxDepth)) { - continue; - } + for (int i = bidCnt; i < prevBidsDepth; i++) { + final Quote quote = bids.removeWorstQuote(); + pool.release(quote); + } - if (marketSide.hasLevel(level)) { - final Quote quote = marketSide.getQuote(level); - mapNewL2ToQuote(l2EntryNewInfo, quote); - } else { - final Quote quote = pool.borrow(); - mapNewL2ToQuote(l2EntryNewInfo, quote); - marketSide.add(level, quote); - } + //Validate state after snapshot + //We believe that snapshot is valid, but... + if (!asks.validateState() || !bids.validateState()) { + clear(); + eventHandler.onBroken(); + return false; + } - if (side == ASK) { - askCnt++; - } else { - bidCnt++; - } + eventHandler.onSnapshot(); + return true; + } - if (askCnt == maxDepth && bidCnt == maxDepth) { - return; - } + @Override + public boolean processBookResetEntry(final PackageHeaderInfo pck, final BookResetEntryInfo msg) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final Option>> exchange = getOrCreateExchange(exchangeId); + + if (exchange.hasValue()) { + clear(); + eventHandler.onReset(); + return true; + } else { + // TODO LOG warning + return false; } - asks.trim(); - bids.trim(); - notWaitingForSnapshot(); } @Override - public void processBookResetEntry(final BookResetEntryInfo bookResetEntryInfo) { - clear(); - waitingForSnapshot(); + public boolean processSecurityFeedStatus(final SecurityFeedStatusMessage msg) { + if (msg.getStatus() == FeedStatus.NOT_AVAILABLE) { + if (disconnectMode == DisconnectMode.CLEAR_EXCHANGE) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final Option>> exchange = getOrCreateExchange(exchangeId); + if (exchange.hasValue()) { + clear(); + eventHandler.onDisconnect(); + return true; + } +// else { +// //TODO LOG warning +// } + } + } + return false; } @Override @@ -252,16 +286,8 @@ public L2MarketSide getMarketSide(final QuoteSide side) { @Override public void clear() { - for (int i = 0; i < asks.depth(); i++) { - final Quote release = asks.getQuote(i); - pool.release(release); - } - asks.clear(); - for (int i = 0; i < bids.depth(); i++) { - final Quote release = bids.getQuote(i); - pool.release(release); - } - bids.clear(); + releaseAndClean(asks); + releaseAndClean(bids); } @Override @@ -269,42 +295,52 @@ public boolean isEmpty() { return asks.isEmpty() && bids.isEmpty(); } + /** + * Check if snapshot is available for processing. + * + * @param msg - snapshot message + * @return true if snapshot is available for processing + */ @Override - public boolean isWaitingForSnapshot() { - return isWaitingForSnapshot; + public boolean isSnapshotAllowed(final PackageHeaderInfo msg) { + final PackageType type = msg.getPackageType(); + return eventHandler.isSnapshotAllowed(type); } - private void waitingForSnapshot() { - if (!isWaitingForSnapshot()) { - isWaitingForSnapshot = true; - } + @Override + public boolean isWaitingForSnapshot() { + return eventHandler.isWaitingForSnapshot(); } - private void notWaitingForSnapshot() { - if (isWaitingForSnapshot()) { - isWaitingForSnapshot = false; + private void releaseAndClean(final L2MarketSide side) { + if (side.isEmpty()) { + return; } - } - - private void checkAndFillGap(final L2EntryNewInfo l2) { - final short depth = l2.getLevel(); - final L2MarketSide marketSide = getMarketSide(l2.getSide()); - final int gaps = depth - marketSide.depth(); - - // If we have a gap between the last existing level and currently inserted level (empty levels between them), - // then let's fill these empty levels with values from the current event. - if (gaps > 0) { - Quote quote; - final short maxDepth = marketSide.getMaxDepth(); - for (int i = 0; i < gaps && marketSide.depth() < maxDepth; i++) { - quote = pool.borrow(); - mapNewL2ToQuote(l2, quote); - marketSide.addLast(quote); - } - marketSide.trim(); + for (int i = 0; i < side.depth(); i++) { + final Quote quote = side.getQuote(i); + pool.release(quote); } + side.clear(); } +// private void checkAndFillGap(final L2EntryNewInfo msg) { +// final int depth = msg.getLevel(); +// final L2MarketSide marketSide = getMarketSide(msg.getSide()); +// final int gaps = depth - marketSide.depth(); +// +// // If we have a gap between the last existing level and currently inserted level (empty levels between them), +// // then let's fill these empty levels with values from the current event. +// if (gaps > 0) { +// Quote quote; +// final int maxDepth = marketSide.getMaxDepth(); +// for (int i = 0; i < gaps && marketSide.depth() < maxDepth; i++) { +// quote = pool.borrow(); +// quote.copyFrom(pck, msg); +// marketSide.addWorstQuote(quote); +// } +// } +// } + /** * Get stock exchange holder by id(create new if it does not exist). * You can create only one exchange. @@ -312,46 +348,19 @@ private void checkAndFillGap(final L2EntryNewInfo l2) { * @param exchangeId - id of exchange. * @return exchange book by id. */ - private Option>> getOrCreateExchange(final long exchangeId) { + private Option>> getOrCreateExchange(@Alphanumeric final long exchangeId) { + if (!AlphanumericUtils.isValidAlphanumeric(exchangeId) || TypeConstants.EXCHANGE_NULL == exchangeId) { + //TODO LOG warning + return Option.empty(); + } + if (!exchanges.isEmpty()) { return exchanges.getById(exchangeId); } - final MutableExchangeImpl> exchange = new MutableExchangeImpl<>(exchangeId, this); + final MutableExchange> exchange = new MutableExchangeImpl<>(exchangeId, this); exchanges.add(exchange); return exchanges.getById(exchangeId); } - /** - * Update quote with L2EntryUpdate. - * - * @param quote - order book quote entry - * @param l2EntryUpdateInfo - L2EntryUpdate - */ - protected void mapUpdateL2ToQuote(final L2EntryUpdateInfo l2EntryUpdateInfo, final Quote quote) { - quote.setSize(l2EntryUpdateInfo.getSize()); - quote.setNumberOfOrders(l2EntryUpdateInfo.getNumberOfOrders()); - } - - /** - * Update quote with L2EntryNew. - * - * @param l2EntryNewInfo - L2EntryNew - * @param quote - quote - */ - protected void mapNewL2ToQuote(final L2EntryNewInfo l2EntryNewInfo, final Quote quote) { - if (quote.getSize() != l2EntryNewInfo.getSize()) { - quote.setSize(l2EntryNewInfo.getSize()); - } - if (quote.getPrice() != l2EntryNewInfo.getPrice()) { - quote.setPrice(l2EntryNewInfo.getPrice()); - } - if (quote.getExchangeId() != l2EntryNewInfo.getExchangeId()) { - quote.setExchangeId(l2EntryNewInfo.getExchangeId()); - } - if (quote.getNumberOfOrders() != l2EntryNewInfo.getNumberOfOrders()) { - quote.setNumberOfOrders(l2EntryNewInfo.getNumberOfOrders()); - } - } - } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3ConsolidatedQuoteProcessor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3ConsolidatedQuoteProcessor.java new file mode 100644 index 0000000..df6f1bb --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3ConsolidatedQuoteProcessor.java @@ -0,0 +1,617 @@ +package com.epam.deltix.orderbook.core.impl; + + +import com.epam.deltix.containers.AlphanumericUtils; +import com.epam.deltix.dfp.Decimal; +import com.epam.deltix.dfp.Decimal64Utils; +import com.epam.deltix.orderbook.core.api.EntryValidationCode; +import com.epam.deltix.orderbook.core.impl.collections.rbt.RBTree; +import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.timebase.messages.TypeConstants; +import com.epam.deltix.timebase.messages.service.FeedStatus; +import com.epam.deltix.timebase.messages.service.SecurityFeedStatusMessage; +import com.epam.deltix.timebase.messages.universal.*; +import com.epam.deltix.util.annotations.Alphanumeric; +import com.epam.deltix.util.collections.generated.ObjectList; + +import java.util.*; + +import static com.epam.deltix.dfp.Decimal64Utils.ZERO; +import static com.epam.deltix.timebase.messages.universal.QuoteSide.ASK; +import static com.epam.deltix.timebase.messages.universal.QuoteSide.BID; + +/** + * @author Andrii_Ostapenko1 + */ +class L3ConsolidatedQuoteProcessor implements L3Processor { + + protected final L3MarketSide bids; + protected final L3MarketSide asks; + + protected final ObjectPool pool; + + protected final MutableExchangeList>> exchanges; + + //Parameters + protected final DisconnectMode disconnectMode; + protected final ValidationOptions validationOptions; + private final OrderBookOptions options; + + L3ConsolidatedQuoteProcessor(final OrderBookOptions options, + final ObjectPool pool) { + this.options = options; + this.validationOptions = options.getInvalidQuoteMode().orElse(Defaults.VALIDATION_OPTIONS); + this.disconnectMode = options.getDisconnectMode().orElse(Defaults.DISCONNECT_MODE); + + final int maxDepth = options.getMaxDepth().orElse(Defaults.MAX_DEPTH); + final int initialDepth = options.getInitialDepth().orElse(Math.min(Defaults.INITIAL_DEPTH, maxDepth)); + final int numberOfExchanges = options.getInitialExchangesPoolSize().orElse(Defaults.INITIAL_EXCHANGES_POOL_SIZE); + this.pool = pool; + this.exchanges = new MutableExchangeListImpl<>(numberOfExchanges); + this.asks = new ConsolidatedL3MarketSide.ASKS<>(numberOfExchanges * initialDepth, Defaults.MAX_DEPTH); + this.bids = new ConsolidatedL3MarketSide.BIDS<>(numberOfExchanges * initialDepth, Defaults.MAX_DEPTH); + } + + @Override + public boolean isWaitingForSnapshot() { + if (exchanges.isEmpty()) { + return true; // No data from exchanges, so we are in "waiting" state + } + + for (final MutableExchange exchange : exchanges) { + if (exchange.isWaitingForSnapshot()) { + return true; // At least one of source exchanges awaits snapshot + } + } + return false; + } + + @Override + public boolean isSnapshotAllowed(final PackageHeaderInfo msg) { + throw new UnsupportedOperationException("Unsupported for multiexchange processor!"); + } + + @Override + public MutableExchangeList>> getExchanges() { + return exchanges; + } + + @Override + public L3MarketSide getMarketSide(final QuoteSide side) { + return side == BID ? bids : asks; + } + + @Override + public boolean isEmpty() { + return asks.isEmpty() && bids.isEmpty(); + } + + @Override + public boolean processSecurityFeedStatus(final SecurityFeedStatusMessage msg) { + if (msg.getStatus() == FeedStatus.NOT_AVAILABLE) { + if (disconnectMode == DisconnectMode.CLEAR_EXCHANGE) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final Option>> holder = getOrCreateExchange(exchangeId); + + if (!holder.hasValue()) { + return false; + } + final L3Processor exchange = holder.get().getProcessor(); + + subtractExchange(exchange); + return exchange.processSecurityFeedStatus(msg); + } + } + return false; + } + + @Override + public boolean processBookResetEntry(final PackageHeaderInfo pck, final BookResetEntryInfo msg) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); + final Option>> holder = getOrCreateExchange(exchangeId); + + if (!holder.hasValue()) { + return false; + } + final L3Processor exchange = holder.get().getProcessor(); + + subtractExchange(exchange); + return exchange.processBookResetEntry(pck, msg); + } + + @Override + public boolean processL3Snapshot(final PackageHeaderInfo msg) { + final ObjectList entries = msg.getEntries(); + + // we assume that all entries in the message are from the same exchange + @Alphanumeric final long exchangeId = entries.get(0).getExchangeId(); + final Option>> holder = getOrCreateExchange(exchangeId); + + if (!holder.hasValue()) { + return false; + } + + final L3Processor exchange = holder.get().getProcessor(); + if (exchange.isSnapshotAllowed(msg)) { + subtractExchange(exchange); + if (exchange.processL3Snapshot(msg)) { + addExchange(exchange); + return true; + } + } + return false; + } + + @Override + public Quote processL3EntryNew(final PackageHeaderInfo pck, final L3EntryNewInfo msg) { + final QuoteSide side = msg.getSide(); + @Alphanumeric final long exchangeId = msg.getExchangeId(); + + final Option>> holder = getOrCreateExchange(exchangeId); + if (!holder.hasValue() || holder.get().getProcessor().isWaitingForSnapshot()) { + return null; + } + + final L3Processor exchange = holder.get().getProcessor(); + final L3MarketSide marketSide = exchange.getMarketSide(side); + final EntryValidationCode errorCode = marketSide.isInvalidInsert(msg.getInsertType(), msg.getQuoteId(), msg.getPrice(), msg.getSize(), side); + if (errorCode != null) { + if (validationOptions.isQuoteInsert()) { + subtractExchange(exchange); + } + exchange.processL3EntryNew(pck, msg); + return null; + } + + final Quote quote; + final L3MarketSide consolidatedMarketSide = getMarketSide(side); + if (marketSide.isFull()) { // CAREFUL! In this case we can't guarantee uniqueness of quoteIds + final Quote worstQuote = marketSide.getWorstQuote(); + if (side == ASK && Decimal64Utils.isGreater(worstQuote.getPrice(), msg.getPrice()) || + side == BID && Decimal64Utils.isGreater(msg.getPrice(), worstQuote.getPrice())) { + quote = marketSide.remove(worstQuote.getQuoteId()); + consolidatedMarketSide.remove(quote); + } else { + return null; + } + } else { + quote = pool.borrow(); + } + + quote.copyFrom(pck, msg); + if (!marketSide.add(quote)) { + pool.release(quote); + if (validationOptions.isQuoteInsert()) { + subtractExchange(exchange); + } + exchange.processL3EntryNew(pck, msg); + return null; + } + consolidatedMarketSide.add(quote); + return quote; + } + + public boolean handleReplace(final L3Processor exchange, + final PackageHeaderInfo pck, + final L3EntryUpdateInfo msg) { + final QuoteSide side = msg.getSide(); + final CharSequence quoteId = msg.getQuoteId(); + final L3MarketSide newSide = exchange.getMarketSide(side); + final L3MarketSide consolidatedNewSide = getMarketSide(side); + + final EntryValidationCode errorCode = newSide.isInvalidInsert(InsertType.ADD_BACK, msg.getQuoteId(), msg.getPrice(), msg.getSize(), side); + if (errorCode != null) { + subtractExchange(exchange); + exchange.processL3EntryUpdate(pck, msg); + } + + final Quote quote = newSide.remove(quoteId); + if (quote != null) { // replace didn't change side + consolidatedNewSide.remove(quote); + quote.copyFrom(pck, msg); + newSide.add(quote); + consolidatedNewSide.add(quote); + return true; + } + + final L3MarketSide prevSide = exchange.getMarketSide(side == ASK ? BID : ASK); + final L3MarketSide consolidatedPrevSide = getMarketSide(side == ASK ? BID : ASK); + final Quote removed = prevSide.remove(quoteId); + if (removed != null) { // replace changed side + consolidatedPrevSide.remove(removed); + Quote newQuote = removed; + if (newSide.isFull()) { + pool.release(removed); + final Quote worstQuote = newSide.getWorstQuote(); + if (side == ASK && Decimal64Utils.isGreater(worstQuote.getPrice(), msg.getPrice()) || + side == BID && Decimal64Utils.isGreater(msg.getPrice(), worstQuote.getPrice())) { + newQuote = newSide.remove(worstQuote.getQuoteId()); + consolidatedNewSide.remove(newQuote); + } else { + return true; + } + } + newQuote.copyFrom(pck, msg); + newSide.add(newQuote); + consolidatedNewSide.add(newQuote); + return true; + } + + if (validationOptions.isQuoteUpdate()) { + subtractExchange(exchange); + } + return exchange.processL3EntryUpdate(pck, msg); + } + + public boolean handleCancel(final L3Processor exchange, + final PackageHeaderInfo pck, + final L3EntryUpdateInfo msg) { + final CharSequence quoteId = msg.getQuoteId(); + final QuoteSide side = msg.getSide() == ASK ? ASK : BID; + + Quote removed = exchange.getMarketSide(side).remove(quoteId); + if (removed == null) { + // setting it as ASK would suffice when side is set correctly or not set at all (null) + removed = exchange.getMarketSide(side == ASK ? BID : ASK).remove(quoteId); + if (removed != null) { + getMarketSide(side == ASK ? BID : ASK).remove(removed); + } + } else { + getMarketSide(side).remove(removed); + } + + if (removed == null) { + if (validationOptions.isQuoteUpdate()) { + subtractExchange(exchange); + } + return exchange.processL3EntryUpdate(pck, msg); + } + pool.release(removed); + return true; + } + + public boolean handleModify(final L3Processor exchange, final PackageHeaderInfo pck, final L3EntryUpdateInfo msg) { + final QuoteSide side = msg.getSide(); // probably we should validate that side != null immediately? + final CharSequence quoteId = msg.getQuoteId(); + final L3MarketSide marketSide = exchange.getMarketSide(side); + final Quote quote = marketSide.getQuote(quoteId); + + final EntryValidationCode errorCode = marketSide.isInvalidUpdate(quote, msg.getQuoteId(), msg.getPrice(), msg.getSize(), side); + if (errorCode != null) { + if (validationOptions.isQuoteUpdate()) { + subtractExchange(exchange); + } + return exchange.processL3EntryUpdate(pck, msg); + } + + quote.copyFrom(pck, msg); + return true; + } + + @Override + public boolean processL3EntryUpdate(final PackageHeaderInfo pck, + final L3EntryUpdateInfo msg) { + @Alphanumeric final long exchangeId = msg.getExchangeId(); + + final Option>> holder = getExchanges().getById(exchangeId); + if (!holder.hasValue() || holder.get().getProcessor().isWaitingForSnapshot()) { + return false; + } + + final L3Processor exchange = holder.get().getProcessor(); + final QuoteUpdateAction action = msg.getAction(); + if (action == QuoteUpdateAction.CANCEL) { + return handleCancel(exchange, pck, msg); + } + if (action == QuoteUpdateAction.REPLACE) { + return handleReplace(exchange, pck, msg); + } + if (action == QuoteUpdateAction.MODIFY) { + return handleModify(exchange, pck, msg); + } + if (validationOptions.isQuoteUpdate()) { + subtractExchange(exchange); + } + return exchange.processL3EntryUpdate(pck, msg); + } + + private void addExchange(final L3Processor exchange) { + { + final L3MarketSide srcMarketSide = exchange.getMarketSide(ASK); + final L3MarketSide dstMarketSide = getMarketSide(ASK); + for (final Quote quote : srcMarketSide) { + dstMarketSide.add(quote); + } + } + + { + final L3MarketSide srcMarketSide = exchange.getMarketSide(BID); + final L3MarketSide dstMarketSide = getMarketSide(BID); + for (final Quote quote : srcMarketSide) { + dstMarketSide.add(quote); + } + } + } + + public void subtractExchange(final L3Processor exchange) { + { + final L3MarketSide srcMarketSide = exchange.getMarketSide(ASK); + final L3MarketSide dstMarketSide = getMarketSide(ASK); + for (final Quote quote : srcMarketSide) { + dstMarketSide.remove(quote); + } + } + + { + final L3MarketSide srcMarketSide = exchange.getMarketSide(BID); + final L3MarketSide dstMarketSide = getMarketSide(BID); + for (final Quote quote : srcMarketSide) { + dstMarketSide.remove(quote); + } + } + } + + /** + * Get stock exchange holder by id(create new if it does not exist). + * + * @param exchangeId - id of exchange. + * @return exchange book by id. + */ + private Option>> getOrCreateExchange(@Alphanumeric final long exchangeId) { + if (!AlphanumericUtils.isValidAlphanumeric(exchangeId) || TypeConstants.EXCHANGE_NULL == exchangeId) { + return Option.empty(); + } + final MutableExchangeList>> exchanges = this.getExchanges(); + Option>> holder = exchanges.getById(exchangeId); + if (!holder.hasValue()) { + final L3Processor processor = new L3SingleExchangeQuoteProcessor<>(options, pool); + exchanges.add(new MutableExchangeImpl<>(exchangeId, processor)); + holder = exchanges.getById(exchangeId); + } + return holder; + } + + @Override + public String getDescription() { + return "L3/Consolidation of multiple exchanges"; + } + + @Override + public void clear() { + asks.clear(); + bids.clear(); + for (final MutableExchange> exchange : this.getExchanges()) { + exchange.getProcessor().clear(); + } + } + + @Override + public String toString() { + return new StringJoiner(", ", L3ConsolidatedQuoteProcessor.class.getSimpleName() + "[", "]") + .add("exchanges=" + exchanges.size()) + .add("bids=" + bids.depth()) + .add("asks=" + asks.depth()) + .toString(); + } + + abstract static class ConsolidatedL3MarketSide implements L3MarketSide { + protected final RBTree data; + private final ReusableIterator itr; + private final int maxDepth; + private long virtualClock; + + ConsolidatedL3MarketSide(final int initialCapacity, final int maxDepth) { + this.maxDepth = maxDepth; + this.data = new RBTree<>(initialCapacity, new QuoteComparator()); + this.itr = new ReusableIterator<>(); + virtualClock = Long.MIN_VALUE; + } + + @Override + public int getMaxDepth() { + return maxDepth; + } + + @Override + public int depth() { + return data.size(); + } + + @Override + public long getTotalQuantity() { + @Decimal long result = ZERO; + for (final Quote quote : this) { + result = Decimal64Utils.add(result, quote.getSize()); + } + return result; + } + + /** + * Clears the market side in linear time + */ + @Override + public void clear() { + data.clear(); + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public Quote getQuote(final int level) { + throw new UnsupportedOperationException(); + } + + @Override + public Quote getQuote(final CharSequence quoteId) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean add(final Quote insert) { + insert.setSequenceNumber(virtualClock++); + final Quote res = data.put(insert, insert); + assert res == null; + return true; + } + + @Override + public Quote remove(final Quote delete) { + final Quote res = data.remove(delete); + assert res != null; + return res; + } + + @Override + public Quote remove(final CharSequence quoteId) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isFull() { + return depth() == maxDepth; + } + + @Override + public Quote getBestQuote() { + if (isEmpty()) { + return null; + } + return data.firstKey(); + } + + @Override + public boolean hasLevel(final int level) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasQuote(final CharSequence quoteId) { + throw new UnsupportedOperationException(); + } + + @Override + public Quote getWorstQuote() { + if (isEmpty()) { + return null; + } + return data.lastKey(); + } + + /** + * @return error code, or null if everything is valid + */ + @Override + public EntryValidationCode isInvalidInsert(final InsertType type, + final CharSequence quoteId, + final @Decimal long price, + final @Decimal long size, + final QuoteSide side) { + throw new UnsupportedOperationException(); + } + + @Override + public EntryValidationCode isInvalidUpdate(final Quote quote, + final CharSequence quoteId, + final @Decimal long price, + final @Decimal long size, + final QuoteSide side) { + throw new UnsupportedOperationException(); + } + + @Override + public void buildFromSorted(final ArrayList quotes) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + for (final Quote quote : this) { + builder.append(quote).append("\n"); + } + return builder.toString(); + } + + @Override + public Iterator iterator(final int fromLevel, final int toLevel) { + if (fromLevel != 0) { + throw new UnsupportedOperationException(); + } + itr.iterateBy(data); + return itr; + } + + /** + * An adapter to safely externalize the value iterator. + */ + static final class ReusableIterator implements Iterator { + + private Iterator> iterator; + + private void iterateBy(final RBTree tm) { + Objects.requireNonNull(tm); + iterator = tm.iterator(); + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Quote next() { + return iterator.next().getValue(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read only iterator"); + } + } + + static class ASKS extends ConsolidatedL3MarketSide { + + ASKS(final int initialDepth, final int maxDepth) { + super(initialDepth, maxDepth); + } + + @Override + public QuoteSide getSide() { + return ASK; + } + + } + + static class BIDS extends ConsolidatedL3MarketSide { + + BIDS(final int initialDepth, final int maxDepth) { + super(initialDepth, maxDepth); + } + + @Override + public QuoteSide getSide() { + return BID; + } + + } + + class QuoteComparator implements Comparator { + + @Override + public int compare(final Quote o1, final Quote o2) { + final int priceComp = Decimal64Utils.compareTo(o1.getPrice(), o2.getPrice()); + if (priceComp == 0) { + return Long.compare(o1.getSequenceNumber(), o2.getSequenceNumber()); + } + if (getSide() == ASK) { + return priceComp; + } else { + return -priceComp; + } + } + } + } +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3MarketSide.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3MarketSide.java new file mode 100644 index 0000000..0a0a732 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3MarketSide.java @@ -0,0 +1,202 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.impl; + +import com.epam.deltix.dfp.Decimal; +import com.epam.deltix.orderbook.core.api.EntryValidationCode; +import com.epam.deltix.orderbook.core.api.MarketSide; +import com.epam.deltix.timebase.messages.universal.InsertType; +import com.epam.deltix.timebase.messages.universal.QuoteSide; + +import java.util.ArrayList; +import java.util.Objects; + +/** + * This interface defines the behaviours of a L3 Market Side. + * + * @param the quote type + */ +interface L3MarketSide extends MarketSide { + /** + * Creates and returns a new instance of {@link L3MarketSide} with the specified initial and maximum depth, + * tailored for the given side of the market (BID or ASK). + * + * This factory method is a convenience for creating instances of {@code L3MarketSide} with either BID or ASK side + * configurations without directly instantiating the concrete implementations. It encapsulates the creation logic + * and ensures that the returned {@code L3MarketSide} is properly initialized with the provided parameters. + * + *

Example Usage:

+ *
{@code
+     * L3MarketSide bidSide = L3MarketSide.factory(10, 100, QuoteSide.BID);
+     * }
+ * + * @param the type parameter extending {@link MutableOrderBookQuote} which specifies the concrete type of the quote used in the order book + * @param initialDepth the initial depth of the order book. Must be non-negative. + * @param maxDepth the maximum depth the order book can grow to. Must be greater than or equal to {@code initialDepth}. + * @param side the side of the order book ({@link QuoteSide#BID} for bids, {@link QuoteSide#ASK} for asks) to determine the market side behavior + * @return a {@link L3MarketSide} instance configured with the specified initial and maximum depth, and market side + * @throws IllegalStateException if the {@code side} is neither {@link QuoteSide#BID} nor {@link QuoteSide#ASK} + * @throws NullPointerException if {@code side} is null + */ + static L3MarketSide factory(final int initialDepth, + final int maxDepth, + final QuoteSide side) { + Objects.requireNonNull(side, "QuoteSide cannot be null."); + switch (side) { + case BID: + return new AbstractL3MarketSide.BIDS<>(initialDepth, maxDepth); + case ASK: + return new AbstractL3MarketSide.ASKS<>(initialDepth, maxDepth); + default: + throw new IllegalStateException("Unexpected value: " + side); + } + } + + /** + * Add a quote to the market side. + * + * @param insert the quote to be inserted + * @return true if the quote was added successfully + */ + boolean add(Quote insert); + + /** + * Get max depth of order book. + * + * @return max depth + */ + int getMaxDepth(); + + /** + * Remove a quote from the market side using its id. + * + * @param quoteId the id of the quote to be removed + * @return the removed quote + */ + Quote remove(CharSequence quoteId); + + /** + * Remove a quote from the market side. + * + * @param quote the quote to be removed + * @return the removed quote + */ + Quote remove(Quote quote); + + /** + * Get quote from the market using its id. + * + * @param quoteId the id of the quote to fetch + * @return the fetched quote + */ + Quote getQuote(CharSequence quoteId); + + /** + * Checks if a quote with given id exists in the market side. + * + * @param quoteId the id of the quote + * @return true if the quote exists + */ + boolean hasQuote(CharSequence quoteId); + + /** + * Checks if the current market side is full. + * + * @return true if the market side is full + */ + boolean isFull(); + + /** + * Clear this market side by removing all quotes. + */ + void clear(); + + /** + * Build this market side from a sorted list of quotes. + * + * @param quotes list of quotes to build the market side + */ + void buildFromSorted(ArrayList quotes); + + /** + * Validates the specified quote parameters before they are inserted into the order book. + * + * This method checks whether the combination of parameters provided for a quote meets the criteria for insertion into the order book. + * It is designed to ensure data integrity and consistency by validating the parameters against predefined rules. + * + * The validation covers various aspects such as the type of insertion, quote identifiers, price and size of the quote, + * and the side of the market the quote is intended for. If any parameter does not comply with the expected criteria, + * an appropriate error code is returned, indicating the reason for validation failure. Otherwise, if the quote is deemed valid, + * a {@code null} value is returned, signifying that the quote can be safely inserted into the order book. + * + *

Usage example:

+ *
{@code
+     * EntryValidationCode validationCode = isInvalidInsert(InsertType.NEW, "quote123", 1000L, 10L, QuoteSide.BID);
+     * if (validationCode != null) {
+     *     // Handle validation failure as per validationCode
+     * } else {
+     *     // Proceed with quote insertion
+     * }
+     * }
+ * + * @param type The type of insertion operation, e.g., NEW, UPDATE, indicating the nature of the action to be performed on the order book. + * @param quoteId A {@link CharSequence} representing the unique identifier of the quote to be validated. + * @param price The price of the quote, which must be a positive long value representing the cost per unit. + * @param size The size of the quote, indicating the number of units, which must be a positive long value. + * @param side The {@link QuoteSide} indicating whether the quote is a bid or an ask in the market. + * @return An {@link EntryValidationCode} enum instance indicating the type of validation error, or {@code null} if the quote is valid. + */ + EntryValidationCode isInvalidInsert(InsertType type, + CharSequence quoteId, + @Decimal long price, + @Decimal long size, + QuoteSide side); + + /** + * Validates the specified quote parameters before updating an existing quote in the order book. + * + * This method assesses whether the updated quote parameters conform to the validation criteria necessary for maintaining the integrity + * and consistency of the order book data. It performs a thorough check on the updated values provided for the quote, including the quote + * identifier, updated price and size values (annotated with {@code @Decimal} to emphasize their decimal nature in financial calculations), + * and the market side (bid or ask) to which the quote belongs. If any updated parameter violates the validation rules, an appropriate error + * code is returned to indicate the nature of the discrepancy. Conversely, if all parameters are deemed valid, the method returns {@code null}, + * signifying that the quote update operation can proceed. + * + *

Usage example:

+ *
{@code
+     * Quote someQuote = getQuoteFromOrderBook("quote123");
+     * EntryValidationCode validationCode = isInvalidUpdate(someQuote, "quote123", 1000L, 50L, QuoteSide.ASK);
+     * if (validationCode != null) {
+     *     // Handle validation error based on the returned code
+     * } else {
+     *     // Proceed with quote update in the order book
+     * }
+     * }
+ * + * @param quote The Quote object representing the quote to be updated. This serves as a reference to the existing quote in the order book. + * @param quoteId A {@link CharSequence} representing the unique identifier of the quote being validated for update. This is used to ensure the correct quote is targeted for update. + * @param price The updated price for the quote, represented as a long value. The {@code @Decimal} annotation indicates its decimal nature, and it must be a positive value. + * @param size The updated size of the quote, representing the number of units, again noted as a long with the {@code @Decimal} annotation, and must be a positive value. + * @param side The {@link QuoteSide} indicating the market side of the quote, either BID or ASK, to which the update applies. + * @return An {@link EntryValidationCode} indicating the validation result: an error code if the updated parameters are invalid, or {@code null} if the update is valid. + */ + EntryValidationCode isInvalidUpdate(Quote quote, + CharSequence quoteId, + @Decimal long price, + @Decimal long size, + QuoteSide side); +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3OrderBookFactory.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3OrderBookFactory.java new file mode 100644 index 0000000..18202ba --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3OrderBookFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.impl; + + +import com.epam.deltix.orderbook.core.api.OrderBook; +import com.epam.deltix.orderbook.core.api.OrderBookQuote; +import com.epam.deltix.orderbook.core.options.Defaults; +import com.epam.deltix.orderbook.core.options.OrderBookOptions; + +/** + * A factory that implements order book for Level3. + * Level3 is Market By Order + *

+ * Not thread safe! + * + * @author Andrii_Ostapenko1 + */ +@SuppressWarnings("unchecked") +public class L3OrderBookFactory { + + /** + * Prevents instantiation + */ + protected L3OrderBookFactory() { + } + + + /** + * Creates OrderBook for single exchange market feed of given initial depth + * + * @param options - options to use + * @param - type of quote + * @return instance of OrderBook for single exchange + */ + public static OrderBook newSingleExchangeBook(final OrderBookOptions options) { + final int maxDepth = options.getMaxDepth().orElse(Defaults.MAX_DEPTH); + final int depth = options.getInitialDepth().orElse(Math.min(Defaults.INITIAL_DEPTH, maxDepth)); + + final ObjectPool pool = + (ObjectPool) options.getSharedObjectPool().orElse(QuotePoolFactory.create(options, depth)); + + final QuoteProcessor processor = new L3SingleExchangeQuoteProcessor<>(options, pool); + return (OrderBook) new OrderBookDecorator<>(options.getSymbol(), processor); + } + + /** + * Creates OrderBook for market feed from multiple exchanges of given maximum depth. + * Consolidated book preserve information about quote's exchange. + * + * @param options - options to use + * @param - type of quote + * @return instance of Order Book with multiple exchanges + */ + public static OrderBook newConsolidatedBook(final OrderBookOptions options) { + final int maxDepth = options.getMaxDepth().orElse(Defaults.MAX_DEPTH); + final int depth = options.getInitialDepth().orElse(Math.min(Defaults.INITIAL_DEPTH, maxDepth)); + final int exchanges = options.getInitialExchangesPoolSize().orElse(Defaults.INITIAL_EXCHANGES_POOL_SIZE); + + final ObjectPool pool = (ObjectPool) options.getSharedObjectPool() + .orElse(QuotePoolFactory.create(options, exchanges * depth)); + + final QuoteProcessor processor = new L3ConsolidatedQuoteProcessor<>(options, pool); + return (OrderBook) new OrderBookDecorator<>(options.getSymbol(), processor); + } +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3Processor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3Processor.java new file mode 100644 index 0000000..baf3599 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3Processor.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.impl; + +import com.epam.deltix.timebase.messages.universal.*; +import com.epam.deltix.util.collections.generated.ObjectList; + +/** + * Processor for L3 universal market data format that included in package (Package Header). + * + * @author Andrii_Ostapenko + */ +interface L3Processor extends QuoteProcessor { + + @Override + default DataModelType getQuoteLevels() { + return DataModelType.LEVEL_THREE; + } + + @Override + L3MarketSide getMarketSide(QuoteSide side); + + /** + * This type reports incremental Level3-new: insert one line in Order Book either on ask or bid side. + * + * @param pck + * @param msg - Level3-new entry + * @return insert quote + */ + Quote processL3EntryNew(PackageHeaderInfo pck, L3EntryNewInfo msg); + + /** + * This type reports incremental Level3-update: update or delete one line in Order Book either on ask or bid side. + * + * @param pck + * @param msg - Level3-update + * @return true if quote was updated, false if quote was not found + */ + boolean processL3EntryUpdate(PackageHeaderInfo pck, L3EntryUpdateInfo msg); + + boolean processL3Snapshot(PackageHeaderInfo msg); + + boolean isSnapshotAllowed(PackageHeaderInfo msg); + + default boolean processIncrementalUpdate(final PackageHeaderInfo pck, final BaseEntryInfo entryInfo) { + if (entryInfo instanceof L3EntryNew) { + final L3EntryNew entry = (L3EntryNew) entryInfo; + return processL3EntryNew(pck, entry) != null; + } else if (entryInfo instanceof L3EntryUpdate) { + final L3EntryUpdate entry = (L3EntryUpdate) entryInfo; + return processL3EntryUpdate(pck, entry); + } else if (entryInfo instanceof StatisticsEntry) { + return true; + } + return false; + } + + default boolean processSnapshot(final PackageHeaderInfo msg) { + final ObjectList entries = msg.getEntries(); + final int n = entries.size(); + // skip statistic entries try to establish if we are dealing with order book reset or normal snapshot + for (int i = 0; i < n; i++) { + final BaseEntryInfo entry = entries.get(i); + if (entry instanceof L3EntryNewInfo) { + return processL3Snapshot(msg); + } else if (entry instanceof BookResetEntryInfo) { + final BookResetEntryInfo resetEntry = (BookResetEntryInfo) entry; + if (resetEntry.getModelType() == getQuoteLevels()) { + return processBookResetEntry(msg, resetEntry); + } + } + } + return false; + } + +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3SingleExchangeQuoteProcessor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3SingleExchangeQuoteProcessor.java new file mode 100644 index 0000000..9f7fd2d --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/L3SingleExchangeQuoteProcessor.java @@ -0,0 +1,400 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.impl; + + +import com.epam.deltix.dfp.Decimal64Utils; +import com.epam.deltix.orderbook.core.api.EntryValidationCode; +import com.epam.deltix.orderbook.core.api.ErrorListener; +import com.epam.deltix.orderbook.core.options.Defaults; +import com.epam.deltix.orderbook.core.options.DisconnectMode; +import com.epam.deltix.orderbook.core.options.OrderBookOptions; +import com.epam.deltix.orderbook.core.options.ValidationOptions; +import com.epam.deltix.timebase.messages.MarketMessageInfo; +import com.epam.deltix.timebase.messages.MessageInfo; +import com.epam.deltix.timebase.messages.TypeConstants; +import com.epam.deltix.timebase.messages.service.FeedStatus; +import com.epam.deltix.timebase.messages.service.SecurityFeedStatusMessage; +import com.epam.deltix.timebase.messages.universal.*; +import com.epam.deltix.util.annotations.Alphanumeric; +import com.epam.deltix.util.collections.generated.ObjectList; + +import java.util.ArrayList; + +import static com.epam.deltix.timebase.messages.universal.QuoteSide.ASK; +import static com.epam.deltix.timebase.messages.universal.QuoteSide.BID; + +/** + * @author Andrii_Ostapenko1 + */ +public class L3SingleExchangeQuoteProcessor implements L3Processor { + + protected final ObjectPool pool; + private final L3MarketSide bids; + private final L3MarketSide asks; + + private final EventHandler eventHandler; + + @Alphanumeric + private long exchangeId = TypeConstants.ALPHANUMERIC_NULL; + + private MutableExchangeList>> exchanges; + + //Parameters + private final ValidationOptions validationOptions; + private final DisconnectMode disconnectMode; + private final ErrorListener errorListener; + + + private final ArrayList asksList; + private final ArrayList bidsList; + + public L3SingleExchangeQuoteProcessor(final OrderBookOptions options, final ObjectPool pool) { + this.disconnectMode = options.getDisconnectMode().orElse(Defaults.DISCONNECT_MODE); + this.validationOptions = options.getInvalidQuoteMode().orElse(Defaults.VALIDATION_OPTIONS); + this.eventHandler = new EventHandlerImpl(options); + this.errorListener = options.getErrorListener().orElse(Defaults.DEFAULT_ERROR_LISTENER); + + this.pool = pool; + + final int maxDepth = options.getMaxDepth().orElse(Defaults.MAX_DEPTH); + final int initialDepth = options.getInitialDepth().orElse(Math.min(Defaults.INITIAL_DEPTH, maxDepth)); + this.asks = L3MarketSide.factory(initialDepth, maxDepth, ASK); + this.bids = L3MarketSide.factory(initialDepth, maxDepth, BID); + this.asksList = new ArrayList<>(initialDepth); + this.bidsList = new ArrayList<>(initialDepth); + } + + @Override + public String getDescription() { + return "L3/Single exchange"; + } + + public void failInsert(final PackageHeaderInfo pck, final EntryValidationCode errorCode) { + if (validationOptions.isQuoteInsert()) { + clear(); + errorListener.onError(pck, errorCode); + eventHandler.onBroken(); + } + } + + @Override + public Quote processL3EntryNew(final PackageHeaderInfo pck, final L3EntryNewInfo msg) { + if (!validExchange(pck, msg.getExchangeId())) { + return null; + } + + if (isWaitingForSnapshot()) { + return null; + } + + final QuoteSide side = msg.getSide(); + final L3MarketSide marketSide = getMarketSide(side); + final EntryValidationCode errorCode = marketSide.isInvalidInsert(msg.getInsertType(), msg.getQuoteId(), msg.getPrice(), msg.getSize(), side); + if (errorCode != null) { + failInsert(pck, errorCode); + return null; + } + + final Quote quote; + if (marketSide.isFull()) { // CAREFUL! In this case we can't guarantee uniqueness of quoteIds + final Quote worstQuote = marketSide.getWorstQuote(); + if (side == ASK && Decimal64Utils.isGreater(worstQuote.getPrice(), msg.getPrice()) || + side == BID && Decimal64Utils.isGreater(msg.getPrice(), worstQuote.getPrice())) { + quote = marketSide.remove(worstQuote.getQuoteId()); + } else { + return null; + } + } else { + quote = pool.borrow(); + } + + quote.copyFrom(pck, msg); + if (!marketSide.add(quote)) { + pool.release(quote); + failInsert(pck, EntryValidationCode.DUPLICATE_QUOTE_ID); + return null; + } + return quote; + } + + public boolean failUpdate(final MarketMessageInfo message, final EntryValidationCode errorCode) { + if (validationOptions.isQuoteUpdate()) { + clear(); + errorListener.onError(message, errorCode); + eventHandler.onBroken(); + return false; + } + return true; // skip invalid update + } + + public boolean handleReplace(final PackageHeaderInfo pck, final L3EntryUpdateInfo msg) { + final QuoteSide side = msg.getSide(); + final CharSequence quoteId = msg.getQuoteId(); + final L3MarketSide newSide = getMarketSide(side); + + final EntryValidationCode errorCode = newSide.isInvalidInsert(InsertType.ADD_BACK, quoteId, msg.getPrice(), msg.getSize(), side); + if (errorCode != null) { + return failUpdate(pck, errorCode); + } + + final Quote quote = newSide.remove(quoteId); + if (quote != null) { // replace didn't change side + quote.copyFrom(pck, msg); + newSide.add(quote); + return true; + } + + final L3MarketSide prevSide = getMarketSide(side == ASK ? BID : ASK); + final Quote removed = prevSide.remove(quoteId); + if (removed != null) { // replace changed side + Quote newQuote = removed; + if (newSide.isFull()) { + pool.release(removed); + final Quote worstQuote = newSide.getWorstQuote(); + if (side == ASK && Decimal64Utils.isGreater(worstQuote.getPrice(), msg.getPrice()) || + side == BID && Decimal64Utils.isGreater(msg.getPrice(), worstQuote.getPrice())) { + newQuote = newSide.remove(worstQuote.getQuoteId()); + } else { + return true; + } + } + newQuote.copyFrom(pck, msg); + newSide.add(newQuote); + return true; + } + + return failUpdate(pck, EntryValidationCode.UNKNOWN_QUOTE_ID); + } + + public boolean handleCancel(final PackageHeaderInfo pck, final L3EntryUpdateInfo msg) { + final CharSequence quoteId = msg.getQuoteId(); + final QuoteSide side = msg.getSide() == ASK ? ASK : BID; + + Quote removed = getMarketSide(side).remove(quoteId); + if (removed == null) { + // setting it as ASK would suffice when side is set correctly or not set at all (null) + removed = getMarketSide(side == ASK ? BID : ASK).remove(quoteId); + } + + if (removed == null) { + return failUpdate(pck, EntryValidationCode.UNKNOWN_QUOTE_ID); + } + pool.release(removed); + return true; + } + + public boolean handleModify(final PackageHeaderInfo pck, final L3EntryUpdateInfo msg) { + final QuoteSide side = msg.getSide(); // probably we should validate that side != null immediately? + final CharSequence quoteId = msg.getQuoteId(); + final L3MarketSide marketSide = getMarketSide(side); + final Quote quote = marketSide.getQuote(quoteId); + + final EntryValidationCode errorCode = marketSide.isInvalidUpdate(quote, msg.getQuoteId(), msg.getPrice(), msg.getSize(), side); + if (errorCode != null) { + return failUpdate(pck, errorCode); + } + + quote.copyFrom(pck, msg); + return true; + } + + @Override + public boolean processL3EntryUpdate(final PackageHeaderInfo pck, final L3EntryUpdateInfo msg) { + if (!validExchange(pck, msg.getExchangeId())) { + return false; + } + + if (isWaitingForSnapshot()) { + return false; + } + + final QuoteUpdateAction action = msg.getAction(); + if (action == QuoteUpdateAction.CANCEL) { + return handleCancel(pck, msg); + } + if (action == QuoteUpdateAction.REPLACE) { + return handleReplace(pck, msg); + } + if (action == QuoteUpdateAction.MODIFY) { + return handleModify(pck, msg); + } + return failUpdate(pck, EntryValidationCode.UNSUPPORTED_UPDATE_ACTION); + } + + @Override + public boolean processL3Snapshot(final PackageHeaderInfo pck) { + if (!isSnapshotAllowed(pck)) { + return false; + } + clear(); + final ObjectList entries = pck.getEntries(); + + asksList.clear(); + bidsList.clear(); + final int len = entries.size(); + for (int i = 0; i < len; i++) { + final BaseEntryInfo e = entries.get(i); + if (e instanceof L3EntryNewInterface) { + final L3EntryNewInterface entry = (L3EntryNewInterface) e; + if (!validExchange(pck, entry.getExchangeId())) { + // We expect that exchangeId is valid and all entries have the same exchangeId + continue; + } + + final QuoteSide side = entry.getSide(); + final L3MarketSide marketSide = getMarketSide(side); + + // Both sides have the same max depth + final int maxDepth = marketSide.getMaxDepth(); + if ((side == ASK && asksList.size() == maxDepth) || (side == BID && bidsList.size() == maxDepth)) { + continue; + } + + // We don't check for duplicate quoteIds, we can do it with hashMap if necessary + final EntryValidationCode errorCode = + marketSide.isInvalidInsert(entry.getInsertType(), entry.getQuoteId(), entry.getPrice(), entry.getSize(), side); + if (errorCode != null) { + if (validationOptions.isQuoteInsert()) { + errorListener.onError(pck, errorCode); + eventHandler.onBroken(); + } + return false; + } + + final Quote quote = pool.borrow(); + quote.copyFrom(pck, entry); + + if (side == ASK) { + quote.setSequenceNumber(asksList.size()); + asksList.add(quote); + } else { + quote.setSequenceNumber(bidsList.size()); + bidsList.add(quote); + } + + if (asksList.size() == maxDepth && bidsList.size() == maxDepth) { + break; + } + } + } + // Bid and ask entries in each snapshot package should be sorted + // from best to worst price, same-priced entries should be in FIFO order + getMarketSide(ASK).buildFromSorted(asksList); + getMarketSide(BID).buildFromSorted(bidsList); + + eventHandler.onSnapshot(); + return true; + } + + @Override + public boolean processBookResetEntry(final PackageHeaderInfo pck, final BookResetEntryInfo msg) { + if (validExchange(pck, msg.getExchangeId())) { + clear(); + eventHandler.onReset(); + return true; + } + return false; + } + + @Override + public boolean processSecurityFeedStatus(final SecurityFeedStatusMessage msg) { + if (msg.getStatus() == FeedStatus.NOT_AVAILABLE) { + if (disconnectMode == DisconnectMode.CLEAR_EXCHANGE) { + if (validExchange(msg, msg.getExchangeId())) { + clear(); + eventHandler.onDisconnect(); + return true; + } else { + return false; + } + } + } + return false; + } + + @Override + public MutableExchangeList>> getExchanges() { + if (exchanges == null && exchangeId != TypeConstants.ALPHANUMERIC_NULL) { + exchanges = new MutableExchangeListImpl<>(); + exchanges.add(new MutableExchangeImpl<>(exchangeId, this)); + } + return exchanges; + } + + @Override + public L3MarketSide getMarketSide(final QuoteSide side) { + return side == BID ? bids : asks; + } + + @Override + public void clear() { + releaseAndClean(asks); + releaseAndClean(bids); + } + + @Override + public boolean isEmpty() { + return asks.isEmpty() && bids.isEmpty(); + } + + /** + * Check if snapshot is available for processing. + * + * @param msg - snapshot message + * @return true if snapshot is available for processing + */ + @Override + public boolean isSnapshotAllowed(final PackageHeaderInfo msg) { + final PackageType type = msg.getPackageType(); + return eventHandler.isSnapshotAllowed(type); + } + + @Override + public boolean isWaitingForSnapshot() { + return eventHandler.isWaitingForSnapshot(); + } + + private void releaseAndClean(final L3MarketSide side) { + if (side.isEmpty()) { + return; + } + for (final Quote quote : side) { + pool.release(quote); + } + side.clear(); + } + + private boolean validExchange(final MessageInfo pck, @Alphanumeric final long exchangeId) { + if (TypeConstants.ALPHANUMERIC_NULL == exchangeId) { + errorListener.onError(pck, EntryValidationCode.MISSING_EXCHANGE_ID); + return false; + } + + if (TypeConstants.ALPHANUMERIC_NULL == this.exchangeId) { + this.exchangeId = exchangeId; + } else { + if (this.exchangeId != exchangeId) { + errorListener.onError(pck, EntryValidationCode.EXCHANGE_ID_MISMATCH); + return false; + } + } + return true; + } + +} + diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchange.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchange.java index 1b69ad1..8705450 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchange.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchange.java @@ -16,6 +16,7 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.orderbook.core.api.Exchange; /** @@ -30,4 +31,11 @@ interface MutableExchange extends Exchange { */ Processor getProcessor(); + /** + * @return true if order book is waiting for snapshot to recover. + * In this state order book appears empty, but corresponding exchange is likely not empty. + * Order book may be in this state initially, or after we market data disconnect, as well as after internal error. + */ + boolean isWaitingForSnapshot(); + } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeImpl.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeImpl.java index 0977829..20e5dd8 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeImpl.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeImpl.java @@ -16,9 +16,11 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.containers.AlphanumericUtils; import com.epam.deltix.orderbook.core.api.MarketSide; import com.epam.deltix.timebase.messages.universal.QuoteSide; +import com.epam.deltix.util.annotations.Alphanumeric; import java.util.Objects; import java.util.StringJoiner; @@ -29,10 +31,11 @@ public class MutableExchangeImpl> implements MutableExchange { + @Alphanumeric private final long exchangeId; private final Processor processor; - public MutableExchangeImpl(final long exchangeId, + public MutableExchangeImpl(@Alphanumeric final long exchangeId, final Processor processor) { Objects.requireNonNull(processor); this.exchangeId = exchangeId; @@ -40,6 +43,7 @@ public MutableExchangeImpl(final long exchangeId, } @Override + @Alphanumeric public long getExchangeId() { return exchangeId; } @@ -54,6 +58,11 @@ public Processor getProcessor() { return processor; } + @Override + public boolean isWaitingForSnapshot() { + return processor.isWaitingForSnapshot(); + } + @Override public String toString() { return new StringJoiner(", ", MutableExchangeImpl.class.getSimpleName() + "[", "]") diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeList.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeList.java index fcede5d..a47d063 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeList.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeList.java @@ -18,6 +18,7 @@ import com.epam.deltix.orderbook.core.api.ExchangeList; + /** * ReadWrite interface for stock exchange list. * diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeListImpl.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeListImpl.java index f419f5d..9a8c2ec 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeListImpl.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableExchangeListImpl.java @@ -16,8 +16,10 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.orderbook.core.api.Exchange; import com.epam.deltix.orderbook.core.options.Option; +import com.epam.deltix.util.annotations.Alphanumeric; import java.util.*; @@ -49,7 +51,15 @@ public void add(final StockExchange exchange) { } @Override - public Option getById(final long exchangeId) { + public Option getById(@Alphanumeric final long exchangeId) { + if (data.size() == 1) { + final Option exchange = data.get(0); + if (exchange.get().getExchangeId() == exchangeId) { + return exchange; + } else { + return Option.empty(); + } + } for (int i = 0; i < data.size(); i++) { final Option exchange = data.get(i); if (exchange.get().getExchangeId() == exchangeId) { @@ -79,7 +89,7 @@ static final class ReusableIterator implements Iterator> data; - private short cursor; + private int cursor; ReusableIterator(final List> data) { this.data = data; @@ -105,7 +115,7 @@ public StockExchange next() { @Override public void remove() { - throw new UnsupportedOperationException("Read only iterator"); + throw new UnsupportedOperationException("Remove is not supported for this iterator implementation!"); } } } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableOrderBookQuote.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableOrderBookQuote.java index ef8b458..c3f402b 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableOrderBookQuote.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/MutableOrderBookQuote.java @@ -18,6 +18,10 @@ import com.epam.deltix.dfp.Decimal; import com.epam.deltix.orderbook.core.api.OrderBookQuote; +import com.epam.deltix.timebase.messages.universal.BasePriceEntryInfo; +import com.epam.deltix.timebase.messages.universal.PackageHeaderInfo; +import com.epam.deltix.util.annotations.Alphanumeric; + /** * ReadWrite interface for Quote(the smallest element in order book). @@ -31,14 +35,14 @@ interface MutableOrderBookQuote extends OrderBookQuote, Comparable { +public final class ObjectPool { private final Supplier factory; - + private final Consumer releaseCallback; private Object[] array; private int size; + //TODO add javadoc public ObjectPool(final int initialSize, final Supplier factory) { + this(initialSize, factory, null); + } + + public ObjectPool(final int initialSize, final Supplier factory, final Consumer releaseCallback) { + if (initialSize < 0) { + throw new IllegalArgumentException("Illegal size: " + initialSize); + } + this.releaseCallback = releaseCallback; + final Object[] array = new Object[(initialSize == 0) ? 1 : initialSize]; for (int i = 0; i < initialSize; i++) { @@ -45,14 +56,15 @@ public ObjectPool(final int initialSize, final Supplier factory) { @SuppressWarnings("unchecked") public T borrow() { - final Object item; if (size > 0) { - item = array[--size]; + final int last = --size; + final Object item = array[last]; + array[last] = null; // clear reference to borrowed item + assert item != null; + return (T) item; } else { - item = factory.get(); + return factory.get(); } - assert item != null; - return (T) item; } public void release(final T item) { @@ -62,8 +74,10 @@ public void release(final T item) { } array[size++] = item; - // TODO why in this place??? - item.release(); + + if (releaseCallback != null) { + releaseCallback.accept(item); + } } } @@ -74,7 +88,7 @@ public int getTotalSize() { /** * Clear all entries and release all cached objects to Java Garbage Collector */ - public void clear() { + private void clear() { Arrays.fill(array, 0, size, null); size = 0; } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/OrderBookDecorator.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/OrderBookDecorator.java index 94f9dd4..0f597fc 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/OrderBookDecorator.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/OrderBookDecorator.java @@ -22,7 +22,8 @@ import com.epam.deltix.orderbook.core.api.MarketSide; import com.epam.deltix.orderbook.core.api.OrderBook; import com.epam.deltix.orderbook.core.options.Option; -import com.epam.deltix.timebase.messages.MarketMessageInfo; +import com.epam.deltix.timebase.messages.MessageInfo; +import com.epam.deltix.timebase.messages.service.SecurityFeedStatusMessage; import com.epam.deltix.timebase.messages.universal.*; import com.epam.deltix.util.collections.generated.ObjectList; @@ -34,7 +35,6 @@ * @author Andrii_Ostapenko */ class OrderBookDecorator> implements OrderBook { - private final Processor processor; private final Option symbol; @@ -46,19 +46,40 @@ class OrderBookDecorator> impleme this.symbol = symbol.orAnother(Option.empty()); } + public static boolean isMarketDatePackage(final MessageInfo msg) { + return msg instanceof PackageHeaderInfo; + } + + public static boolean isSecurityFeedStatusMessage(final MessageInfo msg) { + return msg instanceof SecurityFeedStatusMessage; + } + + public static boolean isSnapshot(final PackageType type) { + return type == PackageType.VENDOR_SNAPSHOT || type == PackageType.PERIODICAL_SNAPSHOT; + } + + public static boolean isIncrementalUpdate(final PackageType type) { + return type == PackageType.INCREMENTAL_UPDATE; + } + @Override - public boolean update(final MarketMessageInfo message) { - if (Objects.isNull(message)) { + public boolean update(final MessageInfo msg) { + if (Objects.isNull(msg)) { return false; } if (symbol.hasValue()) { - if (!CharSequenceUtils.equals(symbol.get(), message.getSymbol())) { + if (!CharSequenceUtils.equals(symbol.get(), msg.getSymbol())) { + //TODO Add logger return false; } } - if (isMarketDatePackage(message)) { - return updateOrderBook((PackageHeaderInfo) message); + if (isMarketDatePackage(msg)) { + return updateOrderBook((PackageHeaderInfo) msg); + } + if (isSecurityFeedStatusMessage(msg)) { + return updateOrderBook((SecurityFeedStatusMessage) msg); } + return false; } @@ -97,42 +118,56 @@ public boolean isEmpty() { return processor.isEmpty(); } - // TODO add package validation! - private boolean updateOrderBook(final PackageHeaderInfo marketMessageInfo) { + @Override + public boolean isWaitingForSnapshot() { + return processor.isWaitingForSnapshot(); + } + + private boolean updateOrderBook(final PackageHeaderInfo msg) { try { - if (!marketMessageInfo.hasEntries()) { + if (!isValid(msg)) { + // TODO add logger return false; - } else if (isIncrementalUpdate(marketMessageInfo.getPackageType())) { - final ObjectList entries = marketMessageInfo.getEntries(); + } else if (isIncrementalUpdate(msg.getPackageType())) { + final ObjectList entries = msg.getEntries(); boolean isProcess = true; for (int i = 0; i < entries.size(); i++) { final BaseEntryInfo pck = entries.get(i); - if (!processor.process(pck)) { + if (!processor.processIncrementalUpdate(msg, pck)) { isProcess = false; } } return isProcess; - } else if (isSnapshot(marketMessageInfo.getPackageType())) { - return processor.processSnapshot(marketMessageInfo); + } else if (isSnapshot(msg.getPackageType())) { + return processor.processSnapshot(msg); } } catch (final Throwable e) { - throw new Error("Internal Error process entries: " + marketMessageInfo.getEntries() + - " Book state: ASK: size: " + getMarketSide(QuoteSide.ASK).depth() + " " + getMarketSide(QuoteSide.ASK) + - " BID: size: " + getMarketSide(QuoteSide.BID).depth() + " " + getMarketSide(QuoteSide.BID), e); + throw new Error("Error processing market data entries:: " + msg.getEntries() + + " Book state: ASK: size: " + getMarketSide(QuoteSide.ASK).depth() + + " BID: size: " + getMarketSide(QuoteSide.BID).depth(), e); } return false; } - private boolean isMarketDatePackage(final MarketMessageInfo marketMessageInfo) { - return marketMessageInfo instanceof PackageHeaderInfo; - } - - private boolean isSnapshot(final PackageType packageType) { - return packageType == PackageType.VENDOR_SNAPSHOT || packageType == PackageType.PERIODICAL_SNAPSHOT; + /** + * Simple validation of package header. + * + * @param msg - package header + * @return true if package header is valid + */ + private boolean isValid(final PackageHeaderInfo msg) { + return msg.hasPackageType() && + msg.hasEntries() && + msg.getEntries().size() > 0; } - private boolean isIncrementalUpdate(final PackageType packageType) { - return packageType == PackageType.INCREMENTAL_UPDATE; + private boolean updateOrderBook(final SecurityFeedStatusMessage msg) { + try { + processor.processSecurityFeedStatus(msg); + } catch (final Throwable e) { + throw new Error("Error processing market status", e); + } + return false; } } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/QuotePoolFactory.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/QuotePoolFactory.java new file mode 100644 index 0000000..d7062ef --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/QuotePoolFactory.java @@ -0,0 +1,56 @@ +package com.epam.deltix.orderbook.core.impl; + + +import com.epam.deltix.orderbook.core.api.OrderBookQuote; +import com.epam.deltix.orderbook.core.options.Defaults; +import com.epam.deltix.orderbook.core.options.OrderBookOptions; +import com.epam.deltix.timebase.messages.universal.DataModelType; + +/** + * @author Andrii_Ostapenko1 + */ +public final class QuotePoolFactory { + + /** + * Prevents instantiation + */ + private QuotePoolFactory() { + } + + //TODO add javadoc + public static ObjectPool create(final OrderBookOptions options, + final int initialSize) { + final ObjectPool pool; + // TODO: need to refactor + final DataModelType quoteLevels = options.getQuoteLevels().get(); + if (options.shouldStoreQuoteTimestamps().orElse(Defaults.SHOULD_STORE_QUOTE_TIMESTAMPS)) { + switch (quoteLevels) { + case LEVEL_ONE: + case LEVEL_TWO: + pool = new ObjectPool<>(initialSize, MutableOrderBookQuoteTimestampImpl::new, MutableOrderBookQuoteTimestampImpl::release); + break; + case LEVEL_THREE: + pool = new ObjectPool<>(initialSize, MutableOrderBookQuoteL3TimestampImpl::new, MutableOrderBookQuoteL3TimestampImpl::release); + break; + default: + throw new IllegalArgumentException("Unsupported book type: " + options.getBookType() + + " for quote levels: " + quoteLevels); + } + } else { + switch (quoteLevels) { + case LEVEL_ONE: + case LEVEL_TWO: + pool = new ObjectPool<>(initialSize, MutableOrderBookQuoteImpl::new, MutableOrderBookQuoteImpl::release); + break; + case LEVEL_THREE: + pool = new ObjectPool<>(initialSize, MutableOrderBookQuoteL3Impl::new, MutableOrderBookQuoteL3Impl::release); + break; + default: + throw new IllegalArgumentException("Unsupported book type: " + options.getBookType() + + " for quote levels: " + quoteLevels); + } + + } + return pool; + } +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/QuoteProcessor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/QuoteProcessor.java index 0e9cfc1..2873cb2 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/QuoteProcessor.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/QuoteProcessor.java @@ -16,9 +16,11 @@ */ package com.epam.deltix.orderbook.core.impl; + import com.epam.deltix.orderbook.core.api.OrderBook; import com.epam.deltix.orderbook.core.options.Option; -import com.epam.deltix.timebase.messages.MarketMessageInfo; +import com.epam.deltix.timebase.messages.MessageInfo; +import com.epam.deltix.timebase.messages.service.SecurityFeedStatusMessage; import com.epam.deltix.timebase.messages.universal.BaseEntryInfo; import com.epam.deltix.timebase.messages.universal.BookResetEntryInfo; import com.epam.deltix.timebase.messages.universal.PackageHeaderInfo; @@ -29,7 +31,7 @@ interface QuoteProcessor extends OrderBook { @Override - default boolean update(final MarketMessageInfo ignore) { + default boolean update(final MessageInfo ignore) { throw new UnsupportedOperationException("Unsupported for processor: " + getDescription()); } @@ -41,10 +43,11 @@ default Option getSymbol() { /** * Process incremental update market data entry. * - * @param pck - Package header container + * @param pck - package header container + * @param entryInfo - base entry container * @return true if process is success */ - boolean process(final BaseEntryInfo pck); + boolean processIncrementalUpdate(PackageHeaderInfo pck, BaseEntryInfo entryInfo); /** * Process only snapshot(VENDOR,PERIODICAL) market data entry. @@ -52,15 +55,27 @@ default Option getSymbol() { * @param marketMessageInfo - Package header container * @return true if process is success */ - boolean processSnapshot(final PackageHeaderInfo marketMessageInfo); + boolean processSnapshot(PackageHeaderInfo marketMessageInfo); /** - * Waiting snapshot don't apply incremental updates before it. + * Process security feed status msg. *

- * This method using for handle book reset entry. + * This msg is used to notify you about connecting to the exchange. * - * @return true if waiting for snapshot and return false if no waiting for snapshot - * @see ResetEntryProcessor#processBookResetEntry(BookResetEntryInfo) + * @param msg - Status feed container + * @return true if process is success + * @see SecurityFeedStatusMessage + */ + boolean processSecurityFeedStatus(SecurityFeedStatusMessage msg); + + /** + * Book Reset is a Special type of entry that communicates that market data provider wants you to clear all entries + * in accumulated order book. Once you receive BookResetEntry you need to wait for the next Snapshot to + * rebuild order book (incremental update messages that may appear before the snapshot are invalid and should be ignored). + * + * @param resetEntry - Book reset entry + * @param pck - Package header + * @return true if process is success */ - boolean isWaitingForSnapshot(); + boolean processBookResetEntry(PackageHeaderInfo pck, BookResetEntryInfo resetEntry); } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/ResetEntryProcessor.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/ResetEntryProcessor.java deleted file mode 100644 index 1358a81..0000000 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/ResetEntryProcessor.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2021 EPAM Systems, Inc - * - * See the NOTICE file distributed with this work for additional information - * regarding copyright ownership. Licensed under the Apache License, - * Version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.epam.deltix.orderbook.core.impl; - -import com.epam.deltix.timebase.messages.universal.BookResetEntryInfo; - -/** - * Processor for reset entry universal market data format that included in package (Package Header). - * - * @author Andrii_Ostapenko - */ -interface ResetEntryProcessor { - - /** - * Book Reset is a Special type of entry that communicates that market data provider wants you to clear all entries - * in accumulated order book. Once you receive BookResetEntry you need to wait for the next Snapshot to - * rebuild order book (incremental update messages that may appear before the snapshot are invalid and should be ignored). - * - * @param bookResetEntryInfo - Book reset entry - */ - void processBookResetEntry(final BookResetEntryInfo bookResetEntryInfo); - -} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/collections/rbt/RBTree.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/collections/rbt/RBTree.java new file mode 100644 index 0000000..1fc5a77 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/impl/collections/rbt/RBTree.java @@ -0,0 +1,984 @@ +package com.epam.deltix.orderbook.core.impl.collections.rbt; + + +import com.epam.deltix.orderbook.core.impl.ObjectPool; + +import java.util.*; + +/** + * A Red-Black tree-based implementation. + * The map is sorted according to the {@linkplain Comparable natural + * ordering} of its keys, or by a {@link Comparator} provided at map + * creation time, depending on which constructor is used. + * + *

This implementation provides guaranteed log(n) time cost for the + * {@code containsKey}, {@code get}, {@code put} and {@code remove} + * operations. Algorithms are adaptations of those in Cormen, Leiserson, and + * Rivest's Introduction to Algorithms. + */ +public class RBTree { + private static final boolean RED = false; + private static final boolean BLACK = true; + /** + * The comparator used to maintain order in this tree map, or + * null if it uses the natural ordering of its keys. + */ + private final Comparator comparator; + private final ObjectPool> pool; + private final EntryIterator entryIterator = new EntryIterator(null); + private Entry root; + /** + * The number of entries in the tree + */ + private int size = 0; + /** + * The number of structural modifications to the tree. + */ + private int modCount = 0; + + // Query Operations + /** + * Index for tracking array traversal in buildFromSorted() + */ + private int currentIndex = 0; + + /** + * Constructs a new, empty tree map, ordered according to the given + * comparator. All keys inserted into the map must be mutually + * comparable by the given comparator: {@code comparator.compare(k1, + * k2)} must not throw a {@code ClassCastException} for any keys + * {@code k1} and {@code k2} in the map. If the user attempts to put + * a key into the map that violates this constraint, the {@code put(Object + * key, Object value)} call will throw a + * {@code ClassCastException}. + * + * @param initialSize initial size for object pool + * @param comparator the comparator that will be used to order this map. + * If {@code null}, the {@linkplain Comparable natural + * ordering} of the keys will be used. + */ + public RBTree(final int initialSize, final Comparator comparator) { + this.comparator = comparator; + pool = new ObjectPool<>(initialSize, Entry::new); + } + + /** + * Test two values for equality. Differs from o1.equals(o2) only in + * that it copes with {@code null} o1 properly. + * @param o1 the first object to be compared for equality + * @param o2 the second object to be compared for equality + * @return {@code true} if the objects are equal or both {@code null}; {@code false} otherwise + */ + static boolean valEquals(final Object o1, final Object o2) { + return (Objects.equals(o1, o2)); + } + + /** + * Returns the key corresponding to the specified Entry. + * + * @param The type parameter representing the key's type in the entry. + * @param e The {@link Entry} from which the key is to be retrieved. Must not be {@code null}. + * @return The key corresponding to the specified Entry {@code e}. + * @throws NoSuchElementException if the provided entry {@code e} is {@code null}, indicating that there is no entry to retrieve a key from. + */ + static K key(final Entry e) { + if (e == null) { + throw new NoSuchElementException(); + } + return e.key; + } + + /** + * Returns the successor of the specified Entry, or null if no such. + * + * @param the type parameter representing the key's type in the entry + * @param the type parameter representing the value's type in the entry + * @param t the entry whose successor is to be found; may be null + * @return the successor of the specified entry if it exists; otherwise, {@code null} + */ + static Entry successor(final Entry t) { + if (t == null) { + return null; + } else if (t.right != null) { + Entry p = t.right; + while (p.left != null) { + p = p.left; + } + return p; + } else { + Entry p = t.parent; + Entry ch = t; + while (p != null && ch == p.right) { + ch = p; + p = p.parent; + } + return p; + } + } + + /** + * Determines the color attribute of a given tree node within Red-Black Tree operations. + *

+ * This utility method is a part of the balancing operations employed by Red-Black Trees during insertion and deletion + * to maintain their balanced tree properties. It simplifies the process by safely handling {@code null} references, + * avoiding the complexity and messiness of null checks within the core algorithms. Directly dealing with {@code null} + * values is a deviation from the approach described in the CLR (Cormen, Leiserson, Rivest, and Stein) textbook, which + * utilizes dummy nil nodes. This adaptation enhances readability and efficiency by removing the need for dummy nodes + * and explicit null checks. + *

+ * + *

+ * By returning a predefined color (black) for {@code null} nodes, this method ensures that the tree's properties + * are correctly maintained during algorithmic operations without introducing the additional complexity of handling + * dummy nodes. This approach is particularly beneficial for operations that rely on the color properties of a node's + * parent or children, which may be {@code null} at the edges of the tree. + *

+ * + * @param the type parameter representing the key's type in the entry + * @param the type parameter representing the value's type in the entry + * @param p The node whose color is to be determined. Can be {@code null}, in which case it is considered black + * to maintain Red-Black Tree properties. + * @return The color of the node {@code p}, with the color black ({@code false}) returned for {@code null} nodes to + * simplify handling at the tree's boundaries. + */ + private static boolean colorOf(final Entry p) { + return (p == null ? BLACK : p.color); + } + + private static Entry parentOf(final Entry p) { + return (p == null ? null : p.parent); + } + + private static void setColor(final Entry p, boolean c) { + if (p != null) { + p.color = c; + } + } + + private static Entry leftOf(final Entry p) { + return (p == null) ? null : p.left; + } + + private static Entry rightOf(final Entry p) { + return (p == null) ? null : p.right; + } + + /** + * Finds the level down to which to assign all nodes BLACK. This is the + * last `full' level of the complete binary tree produced by buildTree. + * The remaining nodes are colored RED. 'This makes a `nice' set of + * color assignments wrt future insertions.' This level number is + * computed by finding the number of splits needed to reach the zeroeth + * node. + * + * @param size the (non-negative) number of keys in the tree to be built + * @return an integer representing the level in the tree down to which all nodes are to be colored black, + * ensuring an optimal structure for future growth + */ + private static int computeRedLevel(final int size) { + return 31 - Integer.numberOfLeadingZeros(size + 1); + } + + /** + * Returns the number of key-value mappings in this map. + * + * @return the number of key-value mappings in this map + */ + public int size() { + return size; + } + + public boolean isEmpty() { + return size() == 0; + } + + // Little utilities + + /** + * Returns the value to which the specified key is mapped, + * or {@code null} if this map contains no mapping for the key. + * + *

More formally, if this map contains a mapping from a key + * {@code k} to a value {@code v} such that {@code key} compares + * equal to {@code k} according to the map's ordering, then this + * method returns {@code v}; otherwise it returns {@code null}. + * (There can be at most one such mapping.) + * + *

A return value of {@code null} does not necessarily + * indicate that the map contains no mapping for the key; it's also + * possible that the map explicitly maps the key to {@code null}. + * The {@code containsKey} operation may be used to + * distinguish these two cases. + * + * @param key the key whose associated value is to be returned + * @return value for a given key + * @throws ClassCastException if the specified key cannot be compared + * with the keys currently in the map + * @throws NullPointerException if the specified key is null + * and this map uses natural ordering, or its comparator + * does not permit null keys + */ + public V get(final Object key) { + final Entry p = getEntry(key); + return (p == null ? null : p.value); + } + + public Comparator comparator() { + return comparator; + } + + /** + * Retrieves the first (lowest) key currently in this map. + * + * This method returns the lowest key stored in the map, based on the map's + * current sorting criteria. It is particularly useful in scenarios where + * an operation needs to start processing or inspection from the very beginning + * of the ordered map. The method assumes that the map is not empty and has at + * least one key-value mapping stored. + * @throws NoSuchElementException {@inheritDoc} + * @return the first (lowest) key currently stored in this map + */ + public K firstKey() { + return key(getFirstEntry()); + } + + // Red-black mechanics + + /** + * @throws NoSuchElementException {@inheritDoc} + * @return the last key currently stored in this map + */ + public K lastKey() { + return key(getLastEntry()); + } + + /** + * Returns this map's entry for the given key, or {@code null} if the map + * does not contain an entry for the key. + * + * @param key The key whose associated entry is to be returned. + * @return this map's entry for the given key, or {@code null} if the map + * does not contain an entry for the key + * @throws ClassCastException if the specified key cannot be compared + * with the keys currently in the map + * @throws NullPointerException if the specified key is null + * and this map uses natural ordering, or its comparator + * does not permit null keys + */ + final Entry getEntry(final Object key) { + // Offload comparator-based version for the sake of performance + if (comparator != null) { + return getEntryUsingComparator(key); + } + if (key == null) { + throw new NullPointerException(); + } + @SuppressWarnings("unchecked") final Comparable k = (Comparable) key; + Entry p = root; + while (p != null) { + final int cmp = k.compareTo(p.key); + if (cmp < 0) { + p = p.left; + } else if (cmp > 0) { + p = p.right; + } else { + return p; + } + } + return null; + } + + /** + * Version of getEntry using comparator. Split off from getEntry + * for performance. (This is not worth doing for most methods + * that are less dependent on comparator performance, but is + * worthwhile here.) + * @param key The key whose associated entry in the tree is to be returned. The type of the key must + * be compatible with the {@code comparator} used by this tree. + * @return The {@code Entry} associated with the given key if it exists or {@code null} if the tree + * does not contain an entry for the key. + * @throws ClassCastException if the specified key's type prevents it from being compared by the + * tree's comparator. + */ + final Entry getEntryUsingComparator(final Object key) { + @SuppressWarnings("unchecked") final K k = (K) key; + final Comparator cpr = comparator; + if (cpr != null) { + Entry p = root; + while (p != null) { + final int cmp = cpr.compare(k, p.key); + if (cmp < 0) { + p = p.left; + } else if (cmp > 0) { + p = p.right; + } else { + return p; + } + } + } + return null; + } + + /** + * Associates the specified value with the specified key in this map. + * If the map previously contained a mapping for the key, the old + * value is replaced. + * + * @param key key with which the specified value is to be associated + * @param value value to be associated with the specified key + * @return the previous value associated with {@code key}, or + * {@code null} if there was no mapping for {@code key}. + * (A {@code null} return can also indicate that the map + * previously associated {@code null} with {@code key}.) + * @throws ClassCastException if the specified key cannot be compared + * with the keys currently in the map + * @throws NullPointerException if the specified key is null + * and this map uses natural ordering, or its comparator + * does not permit null keys + */ + public V put(final K key, final V value) { + Entry t = root; + if (t == null) { + compare(key, key); // type (and possibly null) check + + root = pool.borrow(); + root.set(key, value, null); + size = 1; + modCount++; + return null; + } + int cmp; + Entry parent; + // split comparator and comparable paths + final Comparator cpr = comparator; + if (cpr != null) { + do { + parent = t; + cmp = cpr.compare(key, t.key); + if (cmp < 0) { + t = t.left; + } else if (cmp > 0) { + t = t.right; + } else { + return t.setValue(value); + } + } while (t != null); + } else { + if (key == null) { + throw new NullPointerException(); + } + @SuppressWarnings("unchecked") final Comparable k = (Comparable) key; + do { + parent = t; + cmp = k.compareTo(t.key); + if (cmp < 0) { + t = t.left; + } else if (cmp > 0) { + t = t.right; + } else { + return t.setValue(value); + } + } while (t != null); + } + final Entry e = pool.borrow(); + e.set(key, value, parent); + if (cmp < 0) { + parent.left = e; + } else { + parent.right = e; + } + fixAfterInsertion(e); + size++; + modCount++; + return null; + } + + /** + * Removes the mapping for this key from this RBTree if present. + * + * @param key key for which mapping should be removed + * @return the previous value associated with {@code key}, or + * {@code null} if there was no mapping for {@code key}. + * (A {@code null} return can also indicate that the map + * previously associated {@code null} with {@code key}.) + * @throws ClassCastException if the specified key cannot be compared + * with the keys currently in the map + * @throws NullPointerException if the specified key is null + * and this map uses natural ordering, or its comparator + * does not permit null keys + */ + public V remove(final Object key) { + final Entry p = getEntry(key); + if (p == null) { + return null; + } + + final V oldValue = p.value; + deleteEntry(p); + return oldValue; + } + + /** + * Removes all of the mappings from this map. + * The map will be empty after this call returns. + */ + public void clear() { + for (Entry e = getFirstEntry(); e != null; e = successor(e)) { + pool.release(e); + } + modCount++; + size = 0; + root = null; + } + + /** + * Returns an iterator over the map's entries. + * + * This method provides an {@link Iterator} that allows iterating through all the entries ({@code Map.Entry}) in the map, + * starting with the first entry. The iteration is in the order determined by the specific implementation of the map, + * which could be insertion order, natural ordering of keys, or any other order defined by the map. + * + * Note: The returned iterator supports the {@code remove()} operation if the underlying map supports it. However, + * modifying the map while iterating (except through the iterator's own {@code remove} method) may result in + * {@link ConcurrentModificationException}. + * + *

Usage example:

+ *
{@code
+     * for (Map.Entry entry : mapInstance.iterator()) {
+     *     // Process each entry
+     * }
+     * }
+ * + *

This method initializes or resets the iterator's state before returning it, ensuring that each call to + * {@code iterator()} starts the iteration from the first entry of the map.

+ * + * @return an {@link Iterator} over the map's entries, starting from the first entry. + */ + public Iterator> iterator() { + entryIterator.reset(getFirstEntry()); + return entryIterator; + } + + /** + * Compares two keys using either their natural ordering or a specified {@link Comparator}. + * + * + *

Example usage:

+ *
{@code
+     * // Assuming a comparator set for Strings
+     * int result = compare("apple", "banana"); // Using the comparator
+     *
+     * // Assuming no comparator is set, and keys are Comparable
+     * int result = compare(10, 20); // Using natural ordering
+     * }
+ * + * @param k1 the first object to be compared. + * @param k2 the second object to be compared. + * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, + * or greater than the second. + * @throws ClassCastException if the keys are not {@link Comparable} or are not mutually comparable + * when {@code comparator} is {@code null}. + */ + @SuppressWarnings("unchecked") + final int compare(final Object k1, final Object k2) { + return comparator == null ? + ((Comparable) k1).compareTo((K) k2) : + comparator.compare((K) k1, (K) k2); + } + + /** + * Returns the first Entry in the RBTree (according to the RBTree's + * key-sort function). Returns null if the RBTree is empty. + * + * @return the first entry in the Red-Black Tree if it exists, or {@code null} if the tree is empty + */ + final Entry getFirstEntry() { + Entry p = root; + if (p != null) { + while (p.left != null) { + p = p.left; + } + } + return p; + } + + /** + * Returns the last Entry in the RBTree (according to the RBTree's + * key-sort function). Returns null if the RBTree is empty. + * + * @return The last {@link Entry} in the RBTree, representing the maximum key entry according to the tree's + * sorting criteria. Returns {@code null} if the RBTree is empty, indicating the absence of any entries. + */ + final Entry getLastEntry() { + Entry p = root; + if (p != null) { + while (p.right != null) { + p = p.right; + } + } + return p; + } + + /** + * From CLR + * @param p the node around which the left rotation is to be performed. Must not be null and + * must have a non-null right child. + */ + private void rotateLeft(final Entry p) { + if (p != null) { + final Entry r = p.right; + p.right = r.left; + if (r.left != null) { + r.left.parent = p; + } + r.parent = p.parent; + if (p.parent == null) { + root = r; + } else if (p.parent.left == p) { + p.parent.left = r; + } else { + p.parent.right = r; + } + r.left = p; + p.parent = r; + } + } + + /** + * From CLR + @param p the node around which the right rotation is to be performed. Must not be null and + * must have a non-null right child. + */ + private void rotateRight(final Entry p) { + if (p != null) { + final Entry l = p.left; + p.left = l.right; + if (l.right != null) { + l.right.parent = p; + } + l.parent = p.parent; + if (p.parent == null) { + root = l; + } else if (p.parent.right == p) { + p.parent.right = l; + } else { + p.parent.left = l; + } + l.right = p; + p.parent = l; + } + } + + /** + * Restores the Red-Black Tree properties after a node has been inserted. + * This method applies a series of rotations and recolorings to maintain the Red-Black Tree properties following + * the insertion of a new node. The new node initially colored red might cause conflicts with existing Red-Black + * Tree properties, specifically the rule that two red nodes cannot be consecutive. This method addresses these + * discrepancies through a series of case analyses and adjustments. + * The process involves traversing up the tree from the inserted node, looking at the color properties of + * the node's relatives (parent, grandparent, and uncle). Depending on these properties, the tree undergoes + * specific rotations (left or right) and recoloring to rebalance itself and preserve the Red-Black properties. + * Conditions leading to different operations include: + * 1. If the inserted node's uncle is red, recoloring of the parent, uncle, and grandparent nodes occurs. + * 2. If the inserted node's uncle is black, and the node is situated in a specific pattern (left-right or right-left), + * rotations are performed, and specific nodes are recolored to restore the tree to its proper state. + * As a final step, the root node is always colored black to ensure the Black-Depth property of Red-Black Trees is + * satisfied. + * + * @param x the newly inserted node that may have caused a violation of the Red-Black Tree properties + */ + private void fixAfterInsertion(Entry x) { + x.color = RED; + + while (x != null && x != root && x.parent.color == RED) { + if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { + final Entry y = rightOf(parentOf(parentOf(x))); + if (colorOf(y) == RED) { + setColor(parentOf(x), BLACK); + setColor(y, BLACK); + setColor(parentOf(parentOf(x)), RED); + x = parentOf(parentOf(x)); + } else { + if (x == rightOf(parentOf(x))) { + x = parentOf(x); + rotateLeft(x); + } + setColor(parentOf(x), BLACK); + setColor(parentOf(parentOf(x)), RED); + rotateRight(parentOf(parentOf(x))); + } + } else { + final Entry y = leftOf(parentOf(parentOf(x))); + if (colorOf(y) == RED) { + setColor(parentOf(x), BLACK); + setColor(y, BLACK); + setColor(parentOf(parentOf(x)), RED); + x = parentOf(parentOf(x)); + } else { + if (x == leftOf(parentOf(x))) { + x = parentOf(x); + rotateRight(x); + } + setColor(parentOf(x), BLACK); + setColor(parentOf(parentOf(x)), RED); + rotateLeft(parentOf(parentOf(x))); + } + } + } + root.color = BLACK; + } + + /** + * Delete node p, and then rebalance the tree. + * + * @param p the node to be deleted from the tree + */ + private void deleteEntry(Entry p) { + modCount++; + size--; + + // If strictly internal, copy successor's element to p and then make p + // point to successor. + if (p.left != null && p.right != null) { + final Entry s = successor(p); + p.key = s.key; + p.value = s.value; + p = s; + } // p has 2 children + + // Start fixup at replacement node, if it exists. + final Entry replacement = (p.left != null ? p.left : p.right); + + if (replacement != null) { + // Link replacement to parent + replacement.parent = p.parent; + if (p.parent == null) { + root = replacement; + } else if (p == p.parent.left) { + p.parent.left = replacement; + } else { + p.parent.right = replacement; + } + + // Null out links so they are OK to use by fixAfterDeletion. + p.left = p.right = p.parent = null; + + // Fix replacement + if (p.color == BLACK) { + fixAfterDeletion(replacement); + } + pool.release(p); + } else if (p.parent == null) { // return if we are the only node. + pool.release(p); + root = null; + } else { // No children. Use self as phantom replacement and unlink. + if (p.color == BLACK) { + fixAfterDeletion(p); + } + + if (p.parent != null) { + if (p == p.parent.left) { + p.parent.left = null; + } else if (p == p.parent.right) { + p.parent.right = null; + } + p.parent = null; + } + pool.release(p); + } + } + + /** + * Restores the Red-Black Tree properties after a node has been deleted. + * This method is invoked after a node is deleted to ensure the tree remains balanced and maintains the Red-Black Tree invariants. + * Deletion from a Red-Black Tree can disrupt the tree's balance by removing a black node, potentially violating the properties + * that define a Red-Black Tree. This method addresses such disruptions through a series of corrective rotations and color changes. + * The core idea revolves around fixing potential double black issues that arise when a black node is removed or moved. We treat + * the node {@code x} as carrying an extra black, which could be due to it being a new child of a deleted black node or it being + * a black node that was moved up. The while loop continues until {@code x} is the root (effectively removing the extra black + * from the tree) or until {@code x} is made red-black (balancing the extra black). The main operations include: + * - Rotation and recoloring when the sibling of {@code x} is red, transforming the situation into one of the subsequent cases. + * - Recoloring when both of the sibling's children are black, moving the extra black up the tree. + * - Rotation and recoloring to remove the extra black at {@code x} when the sibling and its closer child are black, but its + * further child is red. + * - Rotation and recoloring to properly redistribute the colors and ensure the parent node carries the extra black, preparing + * for the algorithm to correct higher-up violations. + * These operations are mirrored for both left and right sides of the parent, ensuring symmetry in handling. + * Once the loop exits, if {@code x} was originally marked with an extra black, it is made simply black, restoring the tree's + * properties. This method is critical for maintaining the red-black properties of the tree after deletions, without + * significantly impacting its balanced structure. + * + * @param x the node to start fixing the Red-Black Tree properties from, typically the child node of the deleted node or + * the node that moved into the deleted node's position. + */ + private void fixAfterDeletion(Entry x) { + while (x != root && colorOf(x) == BLACK) { + if (x == leftOf(parentOf(x))) { + Entry sib = rightOf(parentOf(x)); + + if (colorOf(sib) == RED) { + setColor(sib, BLACK); + setColor(parentOf(x), RED); + rotateLeft(parentOf(x)); + sib = rightOf(parentOf(x)); + } + + if (colorOf(leftOf(sib)) == BLACK && + colorOf(rightOf(sib)) == BLACK) { + setColor(sib, RED); + x = parentOf(x); + } else { + if (colorOf(rightOf(sib)) == BLACK) { + setColor(leftOf(sib), BLACK); + setColor(sib, RED); + rotateRight(sib); + sib = rightOf(parentOf(x)); + } + setColor(sib, colorOf(parentOf(x))); + setColor(parentOf(x), BLACK); + setColor(rightOf(sib), BLACK); + rotateLeft(parentOf(x)); + x = root; + } + } else { // symmetric + Entry sib = leftOf(parentOf(x)); + + if (colorOf(sib) == RED) { + setColor(sib, BLACK); + setColor(parentOf(x), RED); + rotateRight(parentOf(x)); + sib = leftOf(parentOf(x)); + } + + if (colorOf(rightOf(sib)) == BLACK && + colorOf(leftOf(sib)) == BLACK) { + setColor(sib, RED); + x = parentOf(x); + } else { + if (colorOf(leftOf(sib)) == BLACK) { + setColor(rightOf(sib), BLACK); + setColor(sib, RED); + rotateLeft(sib); + sib = leftOf(parentOf(x)); + } + setColor(sib, colorOf(parentOf(x))); + setColor(parentOf(x), BLACK); + setColor(leftOf(sib), BLACK); + rotateRight(parentOf(x)); + x = root; + } + } + } + + setColor(x, BLACK); + } + + /** + * Linear time tree building algorithm from sorted data. + * It is assumed that the comparator of the TreeMap is already set prior + * to calling this method. + * + * @param values new entries are created from entries + * in this array. + */ + public void buildFromSorted(final ArrayList values) { + size = values.size(); + currentIndex = 0; + root = buildFromSorted(0, 0, size - 1, computeRedLevel(size), values); + } + + /** + * Recursive "helper method" that does the real work of the + * previous method. Identically named parameters have + * identical definitions. Additional parameters are documented below. + * It is assumed that the comparator and size fields of the TreeMap are + * already set prior to calling this method. (It ignores both fields.) + * + * @param level The current depth in the tree during the recursive build. For the initial call, + * this should be 0. + * @param lo The index of the first element (inclusive) in the current segment being considered + * for this subtree. For the initial call, this would typically be 0. + * @param hi The index of the last element (inclusive) in the current segment for this subtree. + * Initially, this should be set to the size of the sorted array minus one. + * @param redLevel A predetermined depth in the tree at which nodes are colored red to ensure + * the tree satisfies the balancing properties of a Red-Black Tree. This level + * is calculated based on the size of the tree and typically obtained through + * a call to {@code computeRedLevel}. + * @param values An {@link ArrayList} containing the sorted values to be added to the tree. + * This array provides the values in sequence which are then structured into + * the tree format according to the balancing rules of the tree being constructed. + * + * @return An {@code Entry} object representing the root of the constructed subtree for the + * current recursive invocation. When called initially, this result represents the root of + * the entire tree. + * + */ + @SuppressWarnings("unchecked") + private Entry buildFromSorted(final int level, + final int lo, + final int hi, + final int redLevel, + final ArrayList values) { + /* + * Strategy: The root is the middlemost element. To get to it, we + * have to first recursively construct the entire left subtree, + * so as to grab all of its elements. We can then proceed with right + * subtree. + * + * The lo and hi arguments are the minimum and maximum + * indices to pull out of the iterator or stream for current subtree. + * They are not actually indexed, we just proceed sequentially, + * ensuring that items are extracted in corresponding order. + */ + + if (hi < lo) { + return null; + } + + final int mid = (lo + hi) >>> 1; + + Entry left = null; + if (lo < mid) { + left = buildFromSorted(level + 1, lo, mid - 1, redLevel, + values); + } + + final K key = (K) values.get(currentIndex); + final V value = values.get(currentIndex); + + + final Entry middle = pool.borrow(); + middle.set(key, value, null); + + // color nodes in non-full bottommost level red + if (level == redLevel) { + middle.color = RED; + } + + if (left != null) { + middle.left = left; + left.parent = middle; + } + + currentIndex++; + + if (mid < hi) { + final Entry right = buildFromSorted(level + 1, mid + 1, hi, redLevel, values); + middle.right = right; + right.parent = middle; + } + + return middle; + } + + /** + * Node in the Tree. Doubles as a means to pass key-value pairs back to + * user (see Map.Entry). + */ + + static final class Entry implements Map.Entry { + K key; + V value; + Entry left; + Entry right; + Entry parent; + boolean color; + + /** + * Returns the key. + * + * @return the key + */ + public K getKey() { + return key; + } + + /** + * Returns the value associated with the key. + * + * @return the value associated with the key + */ + public V getValue() { + return value; + } + + /** + * Replaces the value currently associated with the key with the given + * value. + * + * @param value the new value to be associated with this entry's key + * @return the old value that was previously associated with the key, or {@code null} if the key + * did not have an associated value prior to this update. Note that a {@code null} return can also indicate that + * the key was explicitly mapped to {@code null}. + */ + public V setValue(final V value) { + final V oldValue = this.value; + this.value = value; + return oldValue; + } + + public boolean equals(final Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + final Map.Entry e = (Map.Entry) o; + + return valEquals(key, e.getKey()) && valEquals(value, e.getValue()); + } + + public int hashCode() { + final int keyHash = (key == null ? 0 : key.hashCode()); + final int valueHash = (value == null ? 0 : value.hashCode()); + return keyHash ^ valueHash; + } + + public String toString() { + return key + "=" + value; + } + + public void set(final K key, final V value, final Entry parent) { + this.key = key; + this.value = value; + this.parent = parent; + this.left = null; + this.right = null; + this.color = BLACK; + } + } + + /** + * RBTree Iterator + */ + final class EntryIterator implements Iterator> { + Entry next; + Entry lastReturned; + int expectedModCount; + + EntryIterator(final Entry first) { + reset(first); + } + + private void reset(final Entry first) { + expectedModCount = modCount; + lastReturned = null; + next = first; + } + + public boolean hasNext() { + return next != null; + } + + public Entry next() { + final Entry e = next; + if (e == null) { + throw new NoSuchElementException(); + } + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + next = successor(e); + lastReturned = e; + return e; + } + } +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/BindOrderBookOptionsBuilder.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/BindOrderBookOptionsBuilder.java index 3585e1d..f851695 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/BindOrderBookOptionsBuilder.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/BindOrderBookOptionsBuilder.java @@ -16,6 +16,10 @@ */ package com.epam.deltix.orderbook.core.options; + +import com.epam.deltix.orderbook.core.api.ErrorListener; +import com.epam.deltix.orderbook.core.api.OrderBookQuote; +import com.epam.deltix.orderbook.core.impl.ObjectPool; import com.epam.deltix.timebase.messages.universal.DataModelType; /** @@ -61,6 +65,16 @@ public interface BindOrderBookOptionsBuilder { */ BindOrderBookOptionsBuilder updateMode(UpdateMode mode); + /** + * What do we do with periodical snapshots? + * + * @param mode to use + * @return builder + * @see Defaults#PERIODICAL_SNAPSHOT_MODE + * @see com.epam.deltix.timebase.messages.universal.PackageType##PERIODICAL_SNAPSHOT + */ + BindOrderBookOptionsBuilder periodicalSnapshotMode(PeriodicalSnapshotMode mode); + /** * Quote levels to use. * @@ -71,25 +85,30 @@ public interface BindOrderBookOptionsBuilder { BindOrderBookOptionsBuilder quoteLevels(DataModelType type); /** - * Order book type to use. + * Should store timestamps of quote updates? + * If you enable this option, additional memory will be allocated for timestamps in each quote. * - * @param type to use + *

+ * When this option is enabled, you may use {@link OrderBookQuote#getOriginalTimestamp()} to get original quote timestamp + * and {@link OrderBookQuote#getTimestamp()} to get timestamp of order book update. For checking if quote has timestamped + * you may use {@link OrderBookQuote#hasOriginalTimestamp()} and {@link OrderBookQuote#hasTimestamp()}. + *

+ * By default, this option is disabled. + * + * @param value flag * @return builder - * @see Defaults#ORDER_BOOK_TYPE + * @see com.epam.deltix.orderbook.core.api.OrderBookQuoteTimestamp */ - BindOrderBookOptionsBuilder orderBookType(OrderBookType type); + BindOrderBookOptionsBuilder shouldStoreQuoteTimestamps(boolean value); /** - * What do we do if we have a gap between the last existing level and the current inserted level (empty levels in between)?. - *

- * Supported for L2 quote level. + * Order book type to use. * - * @param mode to use + * @param type to use * @return builder - * @see Defaults#GAP_MODE - * @see GapMode + * @see Defaults#ORDER_BOOK_TYPE */ - BindOrderBookOptionsBuilder gapMode(GapMode mode); + BindOrderBookOptionsBuilder orderBookType(OrderBookType type); /** * How large initial depth of market should be? @@ -113,15 +132,15 @@ public interface BindOrderBookOptionsBuilder { BindOrderBookOptionsBuilder maxDepth(int value); /** - * What do we do if we have quote level more than maxDepth?. - * Supported for L2 quote level + * What do we do with invalid packets? + * Supported for L2/L1 quote level * * @param mode to use. * @return builder - * @see Defaults#UNREACHABLE_DEPTH_MODE - * @see UnreachableDepthMode + * @see Defaults#VALIDATION_OPTIONS + * @see ValidationOptions */ - BindOrderBookOptionsBuilder unreachableDepthMode(UnreachableDepthMode mode); + BindOrderBookOptionsBuilder validationOptions(ValidationOptions mode); /** * How large initial pool size for stock exchanges should be? @@ -135,4 +154,52 @@ public interface BindOrderBookOptionsBuilder { */ BindOrderBookOptionsBuilder initialExchangesPoolSize(int value); + /** + * How order book will react on disconnect market data event + * + * @param mode to use + * @see Defaults#DISCONNECT_MODE + * @return builder + */ + BindOrderBookOptionsBuilder disconnectMode(DisconnectMode mode); + + /** + * How order book will processing increment update after reset market data event + * + * @param mode to use + * @see Defaults#RESET_MODE + * @return builder + */ + BindOrderBookOptionsBuilder resetMode(ResetMode mode); + + /** + * Custom error logging + * + * @param errorListener custom error listener + * @return builder + */ + BindOrderBookOptionsBuilder errorListener(ErrorListener errorListener); + + //TODO add javadoc + BindOrderBookOptionsBuilder sharedQuotePool(int initialSize); + + //TODO This method may not make sense + + /** + * When defined allows sharing pool of OrderBookQuote objects between multiple order books + * @param sharedObjectPool shared object pool to use + * @return builder + */ + BindOrderBookOptionsBuilder sharedQuotePool(ObjectPool sharedObjectPool); + + /** + * Use compact version of order book? + * If you enable this option, order book will only store prices and sizes in one array (and therefore should be faster) + *

+ * By default, this option is disabled. + * + * @param value flag + * @return builder + */ + BindOrderBookOptionsBuilder isCompactVersion(boolean value); } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/Defaults.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/Defaults.java index 498df4e..9efaa60 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/Defaults.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/Defaults.java @@ -16,6 +16,8 @@ */ package com.epam.deltix.orderbook.core.options; + +import com.epam.deltix.orderbook.core.api.ErrorListener; import com.epam.deltix.timebase.messages.universal.DataModelType; /** @@ -35,20 +37,25 @@ private Defaults() { */ public static final UpdateMode UPDATE_MODE = UpdateMode.WAITING_FOR_SNAPSHOT; + /** + * Default {@link PeriodicalSnapshotMode}. + */ + public static final PeriodicalSnapshotMode PERIODICAL_SNAPSHOT_MODE = PeriodicalSnapshotMode.PROCESS_ALL; + /** * Default {@link DataModelType}. */ public static final DataModelType QUOTE_LEVELS = DataModelType.LEVEL_ONE; /** - * Default {@link OrderBookType}. + * Should Order book store timestamps of quote updates? */ - public static final OrderBookType ORDER_BOOK_TYPE = OrderBookType.SINGLE_EXCHANGE; + public static final boolean SHOULD_STORE_QUOTE_TIMESTAMPS = false; /** - * Default {@link GapMode}. + * Default {@link OrderBookType}. */ - public static final GapMode GAP_MODE = GapMode.SKIP; + public static final OrderBookType ORDER_BOOK_TYPE = OrderBookType.SINGLE_EXCHANGE; /** * The initial depth of market. @@ -61,13 +68,27 @@ private Defaults() { public static final Integer MAX_DEPTH = 32767; /** - * Default {@link UnreachableDepthMode}. + * Default {@link ValidationOptions}. */ - public static final UnreachableDepthMode UNREACHABLE_DEPTH_MODE = UnreachableDepthMode.SKIP; + public static final ValidationOptions VALIDATION_OPTIONS = ValidationOptions.ALL_ENABLED; + + /** + * Default {@link ResetMode}. + */ + public static final ResetMode RESET_MODE = ResetMode.NON_WAITING_FOR_SNAPSHOT; /** * Initial pool size for stock exchanges. */ public static final Integer INITIAL_EXCHANGES_POOL_SIZE = 1; + /** + * Default {@link DisconnectMode}. + */ + public static final DisconnectMode DISCONNECT_MODE = DisconnectMode.CLEAR_EXCHANGE; + + public static final ErrorListener DEFAULT_ERROR_LISTENER = (message, errorCode) + -> System.err.println("Error parsing message for " + message.getSymbol() + " at " + message.getTimeStampMs() + ": " + errorCode); + + } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/DisconnectMode.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/DisconnectMode.java new file mode 100644 index 0000000..1df4860 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/DisconnectMode.java @@ -0,0 +1,19 @@ +package com.epam.deltix.orderbook.core.options; + +/** + * Enumeration of possible values for customization + * of the modes of processing quotes in case of loss of connection with the exchange. + * + * @author Andrii_Ostapenko + */ +public enum DisconnectMode { + /** + * Clear stock exchange data after disconnect. + */ + CLEAR_EXCHANGE, + /** + * Keeping outdated stock exchange data after disconnect. + */ + KEEP_STALE_DATA + +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/GapMode.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/GapMode.java deleted file mode 100644 index ed607b7..0000000 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/GapMode.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2021 EPAM Systems, Inc - * - * See the NOTICE file distributed with this work for additional information - * regarding copyright ownership. Licensed under the Apache License, - * Version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.epam.deltix.orderbook.core.options; - -/** - * An enumeration of possible values for configuring the gaps in stock quotes. - *

- * Note: GapMode working only with INCREMENTAL_UPDATE - * Supported for L2 - * - * @author Andrii_Ostapenko1 - * @see com.epam.deltix.timebase.messages.universal.PackageType - */ -// TODO ADD UNIT TEST!! -public enum GapMode { - - /** - * The operator ignores all gaps in stock quotes. - *

- * If we have a gap between the last existing level and currently inserted level (empty levels between them), - * then let's skip quote. - */ - SKIP, - - /** - * The operator drop all quotes for stock exchange. - *

- * If we have a gap between the last existing level and currently inserted level (empty levels between them), - * then let's skip quote and drop all quote for stock exchange. - */ - SKIP_AND_DROP, - - /** - * The operator fill gaps in stock quotes. - *

- * If we have a gap between the last existing level and currently inserted level (empty levels between them), - * then let's fill these empty levels with values from the current event. - */ - FILL_GAP - -} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/OrderBookOptions.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/OrderBookOptions.java index bc1fe31..c1cac53 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/OrderBookOptions.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/OrderBookOptions.java @@ -16,6 +16,10 @@ */ package com.epam.deltix.orderbook.core.options; + +import com.epam.deltix.orderbook.core.api.ErrorListener; +import com.epam.deltix.orderbook.core.api.OrderBookQuote; +import com.epam.deltix.orderbook.core.impl.ObjectPool; import com.epam.deltix.timebase.messages.universal.DataModelType; /** @@ -32,6 +36,13 @@ public interface OrderBookOptions { */ Option getUpdateMode(); + /** + * Periodical snapshot mode. + * + * @return periodical snapshot mode. + */ + Option getPeriodicalSnapshotMode(); + /** * Stock symbol. * @@ -47,18 +58,18 @@ public interface OrderBookOptions { Option getQuoteLevels(); /** - * Order book type. + * Should store quote timestamps. * - * @return order book mode. + * @return flag. */ - Option getBookType(); + Option shouldStoreQuoteTimestamps(); /** - * Stock quote gap mode. + * Order book type. * - * @return gap mode. + * @return order book mode. */ - Option getGapMode(); + Option getBookType(); /** * Initial depth of market. @@ -75,11 +86,11 @@ public interface OrderBookOptions { Option getMaxDepth(); /** - * Stock quote unreachableDepth mode. + * Invalid quote mode. * * @return unreachableDepth mode. */ - Option getUnreachableDepthMode(); + Option getInvalidQuoteMode(); /** * Initial pool size for stock exchanges. @@ -87,4 +98,34 @@ public interface OrderBookOptions { * @return pool size. */ Option getInitialExchangesPoolSize(); + + /** + * How order book will react on disconnect market data event. + * + * @return disconnectBehaviour. + */ + Option getDisconnectMode(); + + /** + * How order book will processing increment update after reset market data event + * + * @return resetMode + */ + Option getResetMode(); + + //TODO add javadoc + Option getErrorListener(); + + //TODO add javadoc + Option getInitialSharedQuotePoolSize(); + + //TODO add javadoc + Option> getSharedObjectPool(); + + /** + * Whether compact version of L2 order book is used + * + * @return flag + */ + Option isCompactVersion(); } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/OrderBookOptionsBuilder.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/OrderBookOptionsBuilder.java index c025c88..f749d51 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/OrderBookOptionsBuilder.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/OrderBookOptionsBuilder.java @@ -16,22 +16,31 @@ */ package com.epam.deltix.orderbook.core.options; +import com.epam.deltix.orderbook.core.api.ErrorListener; +import com.epam.deltix.orderbook.core.api.OrderBookQuote; +import com.epam.deltix.orderbook.core.impl.ObjectPool; +import com.epam.deltix.orderbook.core.impl.QuotePoolFactory; import com.epam.deltix.timebase.messages.universal.DataModelType; public class OrderBookOptionsBuilder implements OrderBookOptions, BindOrderBookOptionsBuilder { private Option quoteLevels = Option.empty(); + private Option shouldStoreQuoteTimestamps = Option.empty(); private Option bookType = Option.empty(); private Option updateMode = Option.empty(); - private Option gapMode = Option.empty(); + private Option periodicalSnapshotMode = Option.empty(); private Option symbol = Option.empty(); private Option initialDepth = Option.empty(); - private Option maxDepth = Option.empty(); - - private Option unreachableDepthMode = Option.empty(); + private Option unreachableDepthMode = Option.empty(); private Option initialExchangesPoolSize = Option.empty(); private Option otherOptions = Option.empty(); + private Option disconnectMode = Option.empty(); + private Option resetMode = Option.empty(); + private Option errorListener = Option.empty(); + private Option initialSharedQuotePoolSize = Option.empty(); + private Option> sharedObjectPool = Option.empty(); + private Option isCompactVersion = Option.empty(); @Override public BindOrderBookOptionsBuilder parent(final OrderBookOptions other) { @@ -41,6 +50,9 @@ public BindOrderBookOptionsBuilder parent(final OrderBookOptions other) { @Override public OrderBookOptions build() { + if (!sharedObjectPool.hasValue() && initialSharedQuotePoolSize.hasValue()) { + this.sharedObjectPool = Option.wrap(QuotePoolFactory.create(this, initialSharedQuotePoolSize.get())); + } return this; } @@ -59,12 +71,42 @@ public Option getUpdateMode() { } } + @Override + public BindOrderBookOptionsBuilder periodicalSnapshotMode(final PeriodicalSnapshotMode mode) { + this.periodicalSnapshotMode = Option.wrap(mode); + return this; + } + + @Override + public Option getPeriodicalSnapshotMode() { + if (otherOptions.hasValue()) { + return otherOptions.get().getPeriodicalSnapshotMode().orAnother(periodicalSnapshotMode); + } else { + return periodicalSnapshotMode; + } + } + @Override public BindOrderBookOptionsBuilder quoteLevels(final DataModelType type) { this.quoteLevels = Option.wrap(type); return this; } + @Override + public BindOrderBookOptionsBuilder shouldStoreQuoteTimestamps(final boolean value) { + this.shouldStoreQuoteTimestamps = Option.wrap(value); + return this; + } + + @Override + public Option shouldStoreQuoteTimestamps() { + if (otherOptions.hasValue()) { + return otherOptions.get().shouldStoreQuoteTimestamps().orAnother(shouldStoreQuoteTimestamps); + } else { + return shouldStoreQuoteTimestamps; + } + } + @Override public Option getQuoteLevels() { if (otherOptions.hasValue()) { @@ -120,15 +162,15 @@ public Option getMaxDepth() { } @Override - public BindOrderBookOptionsBuilder unreachableDepthMode(final UnreachableDepthMode mode) { + public BindOrderBookOptionsBuilder validationOptions(final ValidationOptions mode) { this.unreachableDepthMode = Option.wrap(mode); return this; } @Override - public Option getUnreachableDepthMode() { + public Option getInvalidQuoteMode() { if (otherOptions.hasValue()) { - return otherOptions.get().getUnreachableDepthMode().orAnother(unreachableDepthMode); + return otherOptions.get().getInvalidQuoteMode().orAnother(unreachableDepthMode); } else { return unreachableDepthMode; } @@ -149,6 +191,7 @@ public Option getInitialExchangesPoolSize() { } } + @Override public BindOrderBookOptionsBuilder symbol(final String symbol) { this.symbol = Option.wrap(symbol); @@ -165,17 +208,93 @@ public Option getSymbol() { } @Override - public BindOrderBookOptionsBuilder gapMode(final GapMode mode) { - this.gapMode = Option.wrap(mode); + public BindOrderBookOptionsBuilder disconnectMode(final DisconnectMode disconnectMode) { + this.disconnectMode = Option.wrap(disconnectMode); + return this; + } + + @Override + public Option getDisconnectMode() { + if (otherOptions.hasValue()) { + return otherOptions.get().getDisconnectMode().orAnother(disconnectMode); + } else { + return disconnectMode; + } + } + + @Override + public BindOrderBookOptionsBuilder errorListener(final ErrorListener errorListener) { + this.errorListener = Option.wrap(errorListener); + return this; + } + + @Override + public Option getErrorListener() { + if (otherOptions.hasValue()) { + return otherOptions.get().getErrorListener().orAnother(errorListener); + } else { + return errorListener; + } + } + + @Override + public Option getInitialSharedQuotePoolSize() { + if (otherOptions.hasValue()) { + return otherOptions.get().getInitialSharedQuotePoolSize().orAnother(initialSharedQuotePoolSize); + } else { + return initialSharedQuotePoolSize; + } + } + + @Override + public BindOrderBookOptionsBuilder sharedQuotePool(int initialSize) { + this.initialSharedQuotePoolSize = Option.wrap(initialSize); return this; } @Override - public Option getGapMode() { + public BindOrderBookOptionsBuilder sharedQuotePool(final ObjectPool sharedObjectPool) { + this.sharedObjectPool = Option.wrap(sharedObjectPool); + return this; + } + + @Override + public Option> getSharedObjectPool() { + if (sharedObjectPool.hasValue()) { + return otherOptions.get().getSharedObjectPool().orAnother(sharedObjectPool); + } else { + return sharedObjectPool; + } + } + + @Override + public BindOrderBookOptionsBuilder isCompactVersion(final boolean value) { + this.isCompactVersion = Option.wrap(value); + return this; + } + + @Override + public Option isCompactVersion() { + if (otherOptions.hasValue()) { + return otherOptions.get().isCompactVersion().orAnother(isCompactVersion); + } else { + return isCompactVersion; + } + } + + @Override + public BindOrderBookOptionsBuilder resetMode(final ResetMode resetMode) { + this.resetMode = Option.wrap(resetMode); + return this; + } + + @Override + public Option getResetMode() { if (otherOptions.hasValue()) { - return otherOptions.get().getGapMode().orAnother(gapMode); + return otherOptions.get().getResetMode().orAnother(resetMode); } else { - return gapMode; + return resetMode; } } + } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/UnreachableDepthMode.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/PeriodicalSnapshotMode.java similarity index 53% rename from orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/UnreachableDepthMode.java rename to orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/PeriodicalSnapshotMode.java index c2aea26..d41830c 100644 --- a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/UnreachableDepthMode.java +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/PeriodicalSnapshotMode.java @@ -17,30 +17,28 @@ package com.epam.deltix.orderbook.core.options; /** - * An enumeration of possible values for configuring the unreachable(level more than max deep limit) level in stock quotes. + * An enumeration of possible values for setting the processing periodic snapshots mode. *

- * Note: UnreachableDepthMode depends on max deep limit - * Supported for L2 + * Modes for processing periodic snapshots. What do we do with periodic snapshots? * - * @author Andrii_Ostapenko1 - * @see BindOrderBookOptionsBuilder#maxDepth(int) + * @see deltix.timebase.api.messages.universal.PackageType#PERIODICAL_SNAPSHOT */ -// TODO ADD UNIT TEST!! -public enum UnreachableDepthMode { +public enum PeriodicalSnapshotMode { /** - * The operator ignores all unreachable levels in stock quotes. - *

- * If we have a level more than available level, then let's skip quote. + * Skip all periodic snapshots. */ - SKIP, + SKIP_ALL, /** - * The operator drop all quotes for stock exchange. - *

- * If we have a level more than available level, - * then let's skip quote and drop all quote for stock exchange. + * Processing of all periodic snapshots. */ - SKIP_AND_DROP, + PROCESS_ALL, + /** + * Processing a periodic snapshot only once when the order book is waiting for a snapshot. + * + * @see deltix.orderbook.core.options.UpdateMode#WAITING_FOR_SNAPSHOT + */ + ONLY_ONE, } diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/Presets.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/Presets.java new file mode 100644 index 0000000..83e60d3 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/Presets.java @@ -0,0 +1,21 @@ +package com.epam.deltix.orderbook.core.options; + +/** + * Sample order book options. + */ +public class Presets { + + /** + * Quote flow style order book. + */ + public static final OrderBookOptions QF_COMMON = new OrderBookOptionsBuilder() + .updateMode(UpdateMode.WAITING_FOR_SNAPSHOT) + .resetMode(ResetMode.WAITING_FOR_SNAPSHOT) + .validationOptions(ValidationOptions.builder() + .validateQuoteInsert() + .skipInvalidQuoteUpdate() + .build()) + .disconnectMode(DisconnectMode.CLEAR_EXCHANGE) + .periodicalSnapshotMode(PeriodicalSnapshotMode.ONLY_ONE) + .build(); +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/ResetMode.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/ResetMode.java new file mode 100644 index 0000000..eeed435 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/ResetMode.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.options; + +/** + * An enumeration of possible values for configuring the reset mode. + *

+ * Book Reset is a Special type of entry that communicates that market data provider wants you to clear all entries + * in accumulated order book. + *

+ * Modes of order book reset. What we will do after receive Book Reset event. + * Waiting snapshot don't apply incremental updates before it or no. + */ +public enum ResetMode { + + /** + * Waiting snapshot before processing incremental update. + */ + WAITING_FOR_SNAPSHOT, + + /** + * Process incremental update without waiting snapshot. + */ + NON_WAITING_FOR_SNAPSHOT +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/Templates.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/Templates.java new file mode 100644 index 0000000..3a844f9 --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/Templates.java @@ -0,0 +1,20 @@ +package com.epam.deltix.orderbook.core.options; + +/** + * @author Andrii_Ostapenko1 + */ +public final class Templates { + + /** + * Utility class. + */ + private Templates() { + throw new IllegalStateException("No instances!"); + } + + public static final OrderBookOptions QUOTE_FLOW = new OrderBookOptionsBuilder() + .resetMode(ResetMode.NON_WAITING_FOR_SNAPSHOT) + .updateMode(UpdateMode.WAITING_FOR_SNAPSHOT) + .disconnectMode(DisconnectMode.CLEAR_EXCHANGE) + .build(); +} diff --git a/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/ValidationOptions.java b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/ValidationOptions.java new file mode 100644 index 0000000..33be09e --- /dev/null +++ b/orderbook-core/src/main/java/com/epam/deltix/orderbook/core/options/ValidationOptions.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.options; + +/** + * An enumeration of possible values for configuring the invalid quote mode. + * Invalid quote is a quote that has a level more than available level, invalid price or invalid size, + * invalid number of orders or unknown exchange. + *

+ * Note: UnreachableDepthMode depends on max deep limit + * Supported for L2 + * + * @author Andrii_Ostapenko1 + * @see BindOrderBookOptionsBuilder#maxDepth(int) + */ +// TODO ADD UNIT TEST!! +public class ValidationOptions { + + public static final ValidationOptions ALL_ENABLED = ValidationOptions.builder() + .validateQuoteInsert() + .validateQuoteUpdate() + .build(); + + private boolean quoteUpdate = false; + + private boolean quoteInsert = false; + + public boolean isQuoteUpdate() { + return quoteUpdate; + } + + public boolean isQuoteInsert() { + return quoteInsert; + } + + public static ValidationOptionsBuilder builder() { + return new ValidationOptionsBuilder(); + } + + public static class ValidationOptionsBuilder { + private final ValidationOptions mode = new ValidationOptions(); + + public ValidationOptionsBuilder skipInvalidQuoteInsert() { + mode.quoteInsert = false; + return this; + } + + public ValidationOptionsBuilder skipInvalidQuoteUpdate() { + mode.quoteUpdate = false; + return this; + } + + public ValidationOptionsBuilder validateQuoteInsert() { + mode.quoteInsert = true; + return this; + } + + public ValidationOptionsBuilder validateQuoteUpdate() { + mode.quoteUpdate = true; + return this; + } + + public ValidationOptions build() { + return mode; + } + } +} diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/CompactL2SingleExchangeOrderBookTest.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/CompactL2SingleExchangeOrderBookTest.java new file mode 100644 index 0000000..c02d32a --- /dev/null +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/CompactL2SingleExchangeOrderBookTest.java @@ -0,0 +1,231 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core; + +import com.epam.deltix.dfp.Decimal; +import com.epam.deltix.dfp.Decimal64Utils; + +import com.epam.deltix.orderbook.core.api.OrderBook; +import com.epam.deltix.orderbook.core.api.OrderBookFactory; +import com.epam.deltix.orderbook.core.api.OrderBookQuote; +import com.epam.deltix.orderbook.core.fwk.AbstractL2QuoteLevelTest; +import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.timebase.messages.universal.DataModelType; +import com.epam.deltix.timebase.messages.universal.PackageType; +import com.epam.deltix.timebase.messages.universal.QuoteSide; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static com.epam.deltix.timebase.messages.universal.PackageType.VENDOR_SNAPSHOT; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * @author Andrii_Ostapenko1 + */ +public class CompactL2SingleExchangeOrderBookTest extends AbstractL2QuoteLevelTest { + + public BindOrderBookOptionsBuilder opt = new OrderBookOptionsBuilder() + .symbol(DEFAULT_SYMBOL) + .orderBookType(OrderBookType.SINGLE_EXCHANGE) + .quoteLevels(DataModelType.LEVEL_TWO) + .initialDepth(10) + .initialExchangesPoolSize(1) + .isCompactVersion(true) + .updateMode(UpdateMode.WAITING_FOR_SNAPSHOT); + + private OrderBook book = OrderBookFactory.create(opt.build()); + + static Stream quoteProvider() { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + final int numberOfOrders = 25; + + final List asks = new ArrayList<>(maxDepth); + for (int level = 0; level < maxDepth; level++) { + asks.add(arguments(maxDepth, + bbo, + QuoteSide.ASK, + (short) level, + bbo + level, + size + level, + numberOfOrders, + true)); + } + final List bids = new ArrayList<>(maxDepth); + for (int level = 0; level < maxDepth; level++) { + bids.add(arguments(maxDepth, + bbo, + QuoteSide.BID, + (short) level, + bbo - level, + size + level, + numberOfOrders, + false + )); + } + return Stream.concat(asks.stream(), bids.stream()); + } + + @Override + public OrderBook getBook() { + return book; + } + + @Override + public void createBook(final OrderBookOptions otherOpt) { + opt.parent(otherOpt); + book = OrderBookFactory.create(opt.build()); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should add new quote in order book") + public void incrementalUpdate_Insert_L2Quote(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final short priceLevel, + final long price, + final long size, + final long numOfOrders) { + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numOfOrders); + simulateL2Insert(COINBASE, side, priceLevel, price, size, numOfOrders); + + final long expectedDepth = maxExchangeDepth + 1; + assertBookSize(side, (int) expectedDepth); + assertPrice(side, priceLevel, price); + assertSize(side, priceLevel, size); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should delete quote in order book") + public void incrementalUpdate_Delete_L2Quote(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final short priceLevel, + final long price, + final long size, + final long numOfOrders) { + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numOfOrders); + simulateL2Delete(side, priceLevel, price, size, numOfOrders); + + assertBookSize(side, maxExchangeDepth - 1); + assertNotEqualPrice(side, priceLevel, price); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should update quote in order book") + public void incrementalUpdate_Update_L2Quote(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final short priceLevel, + final long price, + final long size, + final long numberOfOrders) { + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); + + @Decimal final long updateSize = Decimal64Utils.add(size, Decimal64Utils.TWO); + final long updateNumberOfOrders = numberOfOrders + 1; + + simulateL2Update(side, priceLevel, price, updateSize, updateNumberOfOrders); + + assertPrice(side, priceLevel, price); + assertBookSize(side, maxExchangeDepth); + assertSize(side, priceLevel, updateSize); + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, + mode = EnumSource.Mode.INCLUDE, + names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) + public void snapshot_L2Quote(final PackageType packageType) { + int maxDepth = 10; + final int bbo = 25; + final int size = 5; + final int numOfOrders = 25; + + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + + maxDepth = maxDepth - 4; + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + + maxDepth = maxDepth + 4; + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + + for (int i = 1; i < 10; i++) { + maxDepth = i; + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + } + + for (int i = 9; i >= 1; i--) { + maxDepth = i; + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + } + + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, + mode = EnumSource.Mode.INCLUDE, + names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) + public void resetEntry_L2Quote(final PackageType packageType) { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + final int numberOfOrders = 25; + + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); + simulateResetEntry(COINBASE, packageType); + + assertBookSize(QuoteSide.BID, 0); + assertBookSize(QuoteSide.ASK, 0); + Assertions.assertTrue(book.isEmpty()); + } + + // NOT VERY CLEAN + // but done to avoid changes to the abstract class' test, which is relevant for other order book types + @Override + public void shouldStoreQuoteTimestamp_L1Quote(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final int priceLevel, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + } + +} diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L1SingleExchangeOrderBookTest.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L1SingleExchangeOrderBookTest.java index bddd5fd..172f69c 100644 --- a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L1SingleExchangeOrderBookTest.java +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L1SingleExchangeOrderBookTest.java @@ -16,8 +16,7 @@ */ package com.epam.deltix.orderbook.core; -import com.epam.deltix.dfp.Decimal; -import com.epam.deltix.dfp.Decimal64Utils; + import com.epam.deltix.orderbook.core.api.OrderBook; import com.epam.deltix.orderbook.core.api.OrderBookFactory; import com.epam.deltix.orderbook.core.api.OrderBookQuote; @@ -37,6 +36,7 @@ import java.util.List; import java.util.stream.Stream; + import static com.epam.deltix.timebase.messages.universal.PackageType.VENDOR_SNAPSHOT; import static org.junit.jupiter.params.provider.Arguments.arguments; @@ -62,14 +62,14 @@ static Stream quoteProvider() { final List asks = new ArrayList<>(maxDepth); asks.add(arguments(bbo, QuoteSide.ASK, - Decimal64Utils.fromDouble(bbo - 0.5), - Decimal64Utils.fromDouble(size), + bbo, + size, numberOfOrders)); final List bids = new ArrayList<>(maxDepth); bids.add(arguments(bbo, QuoteSide.BID, - Decimal64Utils.fromDouble(bbo + 0.5), - Decimal64Utils.fromDouble(size), + bbo, + size, numberOfOrders)); return Stream.concat(asks.stream(), bids.stream()); } @@ -90,8 +90,8 @@ public void createBook(final OrderBookOptions otherOpt) { @DisplayName("Should add new quote in order book") public void incrementalUpdate_Insert_L1Quote(final int bbo, final QuoteSide side, - @Decimal final long price, - @Decimal final long size, + final long price, + final long size, final long numOfOrders) { simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, bbo, size, numOfOrders); simulateL1Insert(side, price, size, numOfOrders); @@ -125,7 +125,7 @@ public void resetEntry_L1Quote(final PackageType packageType) { final int numOfOrders = 25; simulateL1QuoteSnapshot(packageType, COINBASE, bbo, size, numOfOrders); - simulateResetEntry(packageType, COINBASE); + simulateResetEntry(COINBASE, packageType); assertBookSize(QuoteSide.BID, 0); assertBookSize(QuoteSide.ASK, 0); diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2AggregatedOrderBookTest.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2AggregatedOrderBookTest.java index ede058b..668e6a2 100644 --- a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2AggregatedOrderBookTest.java +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2AggregatedOrderBookTest.java @@ -27,6 +27,7 @@ import com.epam.deltix.timebase.messages.universal.PackageType; import com.epam.deltix.timebase.messages.universal.QuoteSide; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; @@ -36,7 +37,6 @@ import java.util.List; import java.util.stream.Stream; -import static com.epam.deltix.dfp.Decimal64Utils.multiply; import static org.junit.jupiter.params.provider.Arguments.arguments; /** @@ -66,9 +66,10 @@ static Stream quoteProvider() { bbo, QuoteSide.ASK, (short) level, - Decimal64Utils.fromDouble(bbo + level - 0.5), - Decimal64Utils.fromDouble(size), - numberOfOrders)); + bbo + level, + size, + numberOfOrders, + true)); } final List bids = new ArrayList<>(maxDepth); for (int level = 0; level < maxDepth; level++) { @@ -76,9 +77,10 @@ static Stream quoteProvider() { bbo, QuoteSide.BID, (short) level, - Decimal64Utils.fromDouble(bbo - level + 0.5), - Decimal64Utils.fromDouble(size), - numberOfOrders)); + bbo - level, + size, + numberOfOrders, + false)); } return Stream.concat(asks.stream(), bids.stream()); } @@ -102,24 +104,33 @@ public void incrementUpdate_Insert_L2Quote(final int maxExchangeDepth, final short priceLevel, @Decimal final long price, @Decimal final long size, - final long numberOfOrders) { - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders); - - simulateL2Insert(COINBASE, side, priceLevel, price, size, numberOfOrders); - assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + final long numberOfOrders, + final boolean addStatistics) { + int expectedDepth = maxExchangeDepth; + + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + + double expectedPrice = price; + if (side == QuoteSide.ASK) { + expectedPrice = expectedPrice - 0.1; + } else { + expectedPrice = expectedPrice + 0.1; + } - assertBookSize(side, maxExchangeDepth + 1); + expectedDepth++; - simulateL2Insert(BINANCE, side, priceLevel, price, size, numberOfOrders); - assertExchangeBookSize(BINANCE, side, maxExchangeDepth); + simulateL2Insert(COINBASE, side, priceLevel, expectedPrice, size, numberOfOrders); + assertExchangeBookSize(COINBASE, side, expectedDepth); + assertBookSize(side, expectedDepth); - assertBookSize(side, maxExchangeDepth); + simulateL2Insert(BINANCE, side, priceLevel, expectedPrice, size, numberOfOrders); + assertExchangeBookSize(BINANCE, side, expectedDepth); + assertBookSize(side, expectedDepth); - assertPrice(side, priceLevel, price); - assertSize(side, priceLevel, multiply(size, Decimal64Utils.TWO)); + assertPrice(side, priceLevel, expectedPrice); + assertSize(side, priceLevel, size * 2); assertNumberOfOrders(side, priceLevel, numberOfOrders * 2); - } @ParameterizedTest @@ -130,9 +141,10 @@ public void incrementalUpdate_Delete_L2Quote(final int maxExchangeDepth, final short priceLevel, @Decimal final long price, @Decimal final long size, - final long numberOfOrders) { - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders); + final long numberOfOrders, + final boolean addStatistics) { + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); simulateL2Delete(COINBASE, side, priceLevel, price, size, numberOfOrders); assertExchangeBookSize(COINBASE, side, maxExchangeDepth - 1); @@ -149,30 +161,27 @@ public void incrementUpdate_Update_L2Quote(final int maxExchangeDepth, final int bbo, final QuoteSide side, final short priceLevel, - @Decimal final long price, - @Decimal final long size, - final long numberOfOrders) { - - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders); + final long price, + final long size, + final long numberOfOrders, + final boolean addStatistics) { + final int expectedDepth = maxExchangeDepth; + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); - simulateL2Insert(COINBASE, side, priceLevel, price, size, numberOfOrders); - assertBookSize(side, maxExchangeDepth + 1); - - simulateL2Insert(BINANCE, side, priceLevel, price, size, numberOfOrders); - assertBookSize(side, maxExchangeDepth); @Decimal final long updateSize = Decimal64Utils.add(size, Decimal64Utils.TWO); final long updateNumberOfOrders = numberOfOrders + 1; simulateL2Update(BINANCE, side, priceLevel, price, updateSize, updateNumberOfOrders); - assertBookSize(side, maxExchangeDepth); + assertBookSize(side, expectedDepth); simulateL2Update(COINBASE, side, priceLevel, price, updateSize, updateNumberOfOrders); - assertBookSize(side, maxExchangeDepth); + assertBookSize(side, expectedDepth); - assertPrice(side, priceLevel, price); - assertSize(side, priceLevel, multiply(updateSize, Decimal64Utils.TWO)); + //TODO add strategy to handle updates with different prices in the same level +// assertPrice(side, priceLevel, price); + assertSize(side, priceLevel, updateSize * 2); assertNumberOfOrders(side, priceLevel, updateNumberOfOrders * 2); } @@ -186,8 +195,8 @@ public void snapshot_L2Quote(final PackageType packageType) { final int size = 5; final int numberOfOrders = 25; - simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); - simulateL2QuoteSnapshot(packageType, BINANCE, maxDepth, bbo, size, numberOfOrders); + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders, false); + simulateL2QuoteSnapshot(packageType, BINANCE, maxDepth, bbo, size, numberOfOrders, true); assertBookSize(QuoteSide.BID, maxDepth); assertBookSize(QuoteSide.ASK, maxDepth); @@ -216,10 +225,10 @@ public void resetEntry_L2Quote(final PackageType packageType) { final int size = 5; final int numOfOrders = 25; - simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders); - simulateL2QuoteSnapshot(packageType, BINANCE, maxDepth, bbo, size, numOfOrders); + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders, true); + simulateL2QuoteSnapshot(packageType, BINANCE, maxDepth, bbo, size, numOfOrders, false); - simulateResetEntry(packageType, COINBASE); + simulateResetEntry(COINBASE, packageType); assertIteratorBookQuotes(maxDepth * 2, bbo, size, numOfOrders); assertBookSize(QuoteSide.BID, maxDepth); @@ -228,7 +237,7 @@ public void resetEntry_L2Quote(final PackageType packageType) { assertExchangeBookSize(COINBASE, QuoteSide.ASK, 0); assertExchangeBookSize(COINBASE, QuoteSide.BID, 0); - simulateResetEntry(packageType, BINANCE); + simulateResetEntry(BINANCE, packageType); assertExchangeBookSize(BINANCE, QuoteSide.ASK, 0); assertExchangeBookSize(BINANCE, QuoteSide.BID, 0); @@ -240,4 +249,17 @@ public void resetEntry_L2Quote(final PackageType packageType) { } + @Test + public void isWaitingForSnapshotTest() { + Assertions.assertTrue(book.isWaitingForSnapshot()); // initially we definitely wait for snapshot + + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, 3, 25, 5, 1, true); + Assertions.assertFalse(book.isWaitingForSnapshot()); + + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, 3, 25, 5, 1, false); + Assertions.assertFalse(book.isWaitingForSnapshot()); + + simulateResetEntry(BINANCE, PackageType.VENDOR_SNAPSHOT); + Assertions.assertFalse(book.isWaitingForSnapshot()); + } } diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2ConsolidatedOrderBookTest.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2ConsolidatedOrderBookTest.java index 9fa5855..66771d7 100644 --- a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2ConsolidatedOrderBookTest.java +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2ConsolidatedOrderBookTest.java @@ -27,6 +27,7 @@ import com.epam.deltix.timebase.messages.universal.PackageType; import com.epam.deltix.timebase.messages.universal.QuoteSide; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; @@ -65,9 +66,10 @@ static Stream quoteProvider() { bbo, QuoteSide.ASK, (short) level, - Decimal64Utils.fromDouble(bbo + level - 0.5), - Decimal64Utils.fromDouble(size), - numberOfOrders)); + bbo + level, + size, + numberOfOrders, + false)); } final List bids = new ArrayList<>(maxDepth); for (int level = 0; level < maxDepth; level++) { @@ -75,9 +77,10 @@ static Stream quoteProvider() { bbo, QuoteSide.BID, (short) level, - Decimal64Utils.fromDouble(bbo - level + 0.5), - Decimal64Utils.fromDouble(size), - numberOfOrders)); + bbo - level, + size, + numberOfOrders, + true)); } return Stream.concat(asks.stream(), bids.stream()); } @@ -101,18 +104,22 @@ public void incrementalUpdate_Insert_L2Quote(final int maxExchangeDepth, final short priceLevel, @Decimal final long price, @Decimal final long size, - final long numberOfOrders) { - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders); - assertBookSize(side, maxExchangeDepth * 2); + final long numberOfOrders, + final boolean addStatistics) { + int expctedDepth = maxExchangeDepth * 2; + + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + assertBookSize(side, expctedDepth); simulateL2Insert(COINBASE, side, priceLevel, price, size, numberOfOrders); - assertExchangeBookSize(COINBASE, side, maxExchangeDepth); - assertBookSize(side, maxExchangeDepth * 2); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth + 1); + assertBookSize(side, ++expctedDepth); // We expect that the book size will be increased by 1 simulateL2Insert(BINANCE, side, priceLevel, price, size, numberOfOrders); - assertExchangeBookSize(BINANCE, side, maxExchangeDepth); - assertBookSize(side, maxExchangeDepth * 2); + assertExchangeBookSize(BINANCE, side, maxExchangeDepth + 1); + assertBookSize(side, ++expctedDepth); // We expect that the book size will be increased by 1 + // assertPrice(side, (short) (priceLevel * 2), price); assertSize(side, (short) (priceLevel * 2), size); @@ -127,9 +134,10 @@ public void incrementalUpdate_Delete_L2Quote(final int maxExchangeDepth, final short priceLevel, @Decimal final long price, @Decimal final long size, - final long numberOfOrders) { - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders); + final long numberOfOrders, + final boolean addStatistics) { + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); simulateL2Delete(COINBASE, side, priceLevel, price, size, numberOfOrders); assertExchangeBookSize(COINBASE, side, maxExchangeDepth - 1); @@ -148,25 +156,25 @@ public void incrementalUpdate_Update_L2Quote(final int maxExchangeDepth, final short priceLevel, @Decimal final long price, @Decimal final long size, - final long numberOfOrders) { - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); - simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders); - - simulateL2Insert(COINBASE, side, priceLevel, price, size, numberOfOrders); - assertBookSize(side, maxExchangeDepth * 2); - - simulateL2Insert(BINANCE, side, priceLevel, price, size, numberOfOrders); - assertBookSize(side, maxExchangeDepth * 2); + final long numberOfOrders, + final boolean addStatistics) { + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + final int exchangeDepth = maxExchangeDepth * 2; @Decimal final long updateSize = Decimal64Utils.add(size, Decimal64Utils.TWO); final long updateNumberOfOrders = numberOfOrders + 1; simulateL2Update(BINANCE, side, priceLevel, price, updateSize, updateNumberOfOrders); - assertBookSize(side, maxExchangeDepth * 2); + assertExchangeBookSize(BINANCE, side, maxExchangeDepth); + assertBookSize(side, exchangeDepth); simulateL2Update(COINBASE, side, priceLevel, price, updateSize, updateNumberOfOrders); - assertBookSize(side, maxExchangeDepth * 2); + assertExchangeBookSize(BINANCE, side, maxExchangeDepth); + assertBookSize(side, exchangeDepth); + //TODO add strategy to handle updates with different prices in the same level +// assertPrice(side, (short) (priceLevel * 2), price); assertSize(side, (short) (priceLevel * 2), updateSize); assertNumberOfOrders(side, (short) (priceLevel * 2), updateNumberOfOrders); } @@ -181,8 +189,8 @@ public void snapshot_L2Quote(final PackageType packageType) { final int size = 5; final int numberOfOrders = 25; - simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); - simulateL2QuoteSnapshot(packageType, BINANCE, maxDepth, bbo, size, numberOfOrders); + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders, true); + simulateL2QuoteSnapshot(packageType, BINANCE, maxDepth, bbo, size, numberOfOrders, false); assertBookSize(QuoteSide.BID, maxDepth * 2); assertBookSize(QuoteSide.ASK, maxDepth * 2); @@ -202,10 +210,10 @@ public void resetEntry_L2Quote(final PackageType packageType) { final int size = 5; final int numberOfOrders = 25; - simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); - simulateL2QuoteSnapshot(packageType, BINANCE, maxDepth, bbo, size, numberOfOrders); + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders, false); + simulateL2QuoteSnapshot(packageType, BINANCE, maxDepth, bbo, size, numberOfOrders, true); - simulateResetEntry(packageType, COINBASE); + simulateResetEntry(COINBASE, packageType); assertIteratorBookQuotes(maxDepth * 2, bbo, size, numberOfOrders); assertBookSize(QuoteSide.BID, maxDepth); @@ -214,7 +222,7 @@ public void resetEntry_L2Quote(final PackageType packageType) { assertExchangeBookSize(COINBASE, QuoteSide.ASK, 0); assertExchangeBookSize(COINBASE, QuoteSide.BID, 0); - simulateResetEntry(packageType, BINANCE); + simulateResetEntry(BINANCE, packageType); assertExchangeBookSize(BINANCE, QuoteSide.ASK, 0); assertExchangeBookSize(BINANCE, QuoteSide.BID, 0); @@ -224,4 +232,18 @@ public void resetEntry_L2Quote(final PackageType packageType) { Assertions.assertTrue(book.isEmpty()); } + + @Test + public void isWaitingForSnapshotTest() { + Assertions.assertTrue(book.isWaitingForSnapshot()); // initially we definitely wait for snapshot + + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, 3, 25, 5, 1, true); + Assertions.assertFalse(book.isWaitingForSnapshot()); + + simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, BINANCE, 3, 25, 5, 1, false); + Assertions.assertFalse(book.isWaitingForSnapshot()); + + simulateResetEntry(BINANCE, PackageType.VENDOR_SNAPSHOT); + Assertions.assertFalse(book.isWaitingForSnapshot()); + } } diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2SingleExchangeOrderBookTest.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2SingleExchangeOrderBookTest.java index d479a77..2e1b860 100644 --- a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2SingleExchangeOrderBookTest.java +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L2SingleExchangeOrderBookTest.java @@ -18,6 +18,7 @@ import com.epam.deltix.dfp.Decimal; import com.epam.deltix.dfp.Decimal64Utils; + import com.epam.deltix.orderbook.core.api.OrderBook; import com.epam.deltix.orderbook.core.api.OrderBookFactory; import com.epam.deltix.orderbook.core.api.OrderBookQuote; @@ -67,9 +68,10 @@ static Stream quoteProvider() { bbo, QuoteSide.ASK, (short) level, - Decimal64Utils.fromDouble(bbo + level - 0.5), - Decimal64Utils.fromDouble(size), - numberOfOrders)); + bbo + level, + size + level, + numberOfOrders, + true)); } final List bids = new ArrayList<>(maxDepth); for (int level = 0; level < maxDepth; level++) { @@ -77,9 +79,10 @@ static Stream quoteProvider() { bbo, QuoteSide.BID, (short) level, - Decimal64Utils.fromDouble(bbo - level + 0.5), - Decimal64Utils.fromDouble(size), - numberOfOrders)); + bbo - level, + size + level, + numberOfOrders, + false)); } return Stream.concat(asks.stream(), bids.stream()); } @@ -102,13 +105,15 @@ public void incrementalUpdate_Insert_L2Quote(final int maxExchangeDepth, final int bbo, final QuoteSide side, final short priceLevel, - @Decimal final long price, - @Decimal final long size, - final long numOfOrders) { - simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numOfOrders); + final long price, + final long size, + final long numOfOrders, + final boolean addStatistics) { + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numOfOrders, addStatistics); simulateL2Insert(COINBASE, side, priceLevel, price, size, numOfOrders); - assertBookSize(side, maxExchangeDepth); + final long expectedDepth = maxExchangeDepth + 1; + assertBookSize(side, (int) expectedDepth); assertEqualLevel(side, priceLevel, price, size, numOfOrders); } @@ -119,10 +124,11 @@ public void incrementalUpdate_Delete_L2Quote(final int maxExchangeDepth, final int bbo, final QuoteSide side, final short priceLevel, - @Decimal final long price, - @Decimal final long size, - final long numOfOrders) { - simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numOfOrders); + final long price, + final long size, + final long numOfOrders, + final boolean addStatistics) { + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numOfOrders, addStatistics); simulateL2Delete(side, priceLevel, price, size, numOfOrders); assertBookSize(side, maxExchangeDepth - 1); @@ -136,16 +142,18 @@ public void incrementalUpdate_Update_L2Quote(final int maxExchangeDepth, final int bbo, final QuoteSide side, final short priceLevel, - @Decimal final long price, - @Decimal final long size, - final long numberOfOrders) { - simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); + final long price, + final long size, + final long numberOfOrders, + final boolean addStatistics) { + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); @Decimal final long updateSize = Decimal64Utils.add(size, Decimal64Utils.TWO); final long updateNumberOfOrders = numberOfOrders + 1; simulateL2Update(side, priceLevel, price, updateSize, updateNumberOfOrders); + assertEqualLevel(side, priceLevel, price, updateSize, updateNumberOfOrders); assertBookSize(side, maxExchangeDepth); assertSize(side, priceLevel, updateSize); assertNumberOfOrders(side, priceLevel, updateNumberOfOrders); @@ -156,14 +164,39 @@ public void incrementalUpdate_Update_L2Quote(final int maxExchangeDepth, mode = EnumSource.Mode.INCLUDE, names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) public void snapshot_L2Quote(final PackageType packageType) { - final int maxDepth = 10; + int maxDepth = 10; final int bbo = 25; final int size = 5; final int numOfOrders = 25; - simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders); + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders, true); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + + maxDepth = maxDepth - 4; + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders, false); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + + maxDepth = maxDepth + 4; + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders, true); assertBookSize(QuoteSide.BID, maxDepth); assertBookSize(QuoteSide.ASK, maxDepth); + + for (int i = 1; i < 10; i++) { + maxDepth = i; + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders, false); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + } + + for (int i = 9; i >= 1; i--) { + maxDepth = i; + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numOfOrders, true); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + } + } @ParameterizedTest @@ -176,8 +209,8 @@ public void resetEntry_L2Quote(final PackageType packageType) { final int size = 5; final int numberOfOrders = 25; - simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); - simulateResetEntry(packageType, COINBASE); + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders, true); + simulateResetEntry(COINBASE, packageType); assertBookSize(QuoteSide.BID, 0); assertBookSize(QuoteSide.ASK, 0); diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L3ConsolidatedOrderBookTest.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L3ConsolidatedOrderBookTest.java new file mode 100644 index 0000000..49bcbe6 --- /dev/null +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L3ConsolidatedOrderBookTest.java @@ -0,0 +1,318 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core; + +import com.epam.deltix.dfp.Decimal64Utils; +import com.epam.deltix.orderbook.core.api.OrderBook; +import com.epam.deltix.orderbook.core.api.OrderBookFactory; +import com.epam.deltix.orderbook.core.api.OrderBookQuote; +import com.epam.deltix.orderbook.core.fwk.AbstractL3QuoteLevelTest; +import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.timebase.messages.universal.DataModelType; +import com.epam.deltix.timebase.messages.universal.InsertType; +import com.epam.deltix.timebase.messages.universal.PackageType; +import com.epam.deltix.timebase.messages.universal.QuoteSide; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +import static com.epam.deltix.timebase.messages.universal.PackageType.VENDOR_SNAPSHOT; + + +/** + * @author Andrii_Ostapenko1 + */ +public class L3ConsolidatedOrderBookTest extends AbstractL3QuoteLevelTest { + + public BindOrderBookOptionsBuilder opt = new OrderBookOptionsBuilder() + .symbol(DEFAULT_SYMBOL) + .orderBookType(OrderBookType.CONSOLIDATED) + .quoteLevels(DataModelType.LEVEL_THREE) + .initialDepth(10) + .initialExchangesPoolSize(1) + .updateMode(UpdateMode.WAITING_FOR_SNAPSHOT); + + private OrderBook book = OrderBookFactory.create(opt.build()); + + @Override + public OrderBook getBook() { + return book; + } + + @Override + public void createBook(final OrderBookOptions otherOpt) { + opt.parent(otherOpt); + book = OrderBookFactory.create(opt.build()); + } + + @Test + public void incrementalUpdate_WrongExchange_L3Quote() { + final QuoteSide side = QuoteSide.ASK; + final int maxDepth = 10; + final long bbo = -256; + final long size = 10; + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, "id" + maxDepth, InsertType.ADD_BACK, null, bbo + 10, size); + simulateCancel(BINANCE, "id" + maxDepth); + assertBookSize(side, maxDepth + 1); + assertExchangeBookSize(BINANCE, side, 0); + } + + @Test + public void incrementalUpdate_MissingExchange_L3Quote() { + final QuoteSide side = QuoteSide.ASK; + final int maxDepth = 10; + final long bbo = -256; + final long size = 10; + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + + simulateInsert(DEFAULT_SYMBOL, BINANCE, side, "id" + maxDepth, InsertType.ADD_BACK, null, bbo + 10, size); + simulateCancel(COINBASE, "id" + maxDepth); + assertBookSize(side, maxDepth + 1); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementalUpdate_Insert_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + int totalDepth = maxDepth * 2; + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + assertBookSize(side, totalDepth); + + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, "id" + maxDepth, InsertType.ADD_BACK, null, price, size); + assertExchangeBookSize(COINBASE, side, maxDepth + 1); + assertBookSize(side, ++totalDepth); // We expect that the book size will be increased by 1 + + simulateInsert(DEFAULT_SYMBOL, BINANCE, side, "id" + maxDepth, InsertType.ADD_BACK, null, price, size); + assertExchangeBookSize(BINANCE, side, maxDepth + 1); + assertBookSize(side, ++totalDepth); // We expect that the book size will be increased by 1 + + final long pos = side == QuoteSide.BID ? bbo - price : price - bbo; + assertEqualPosition(side, (short) (2 * pos), Decimal64Utils.fromLong(price), size, quoteId); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementalUpdate_invalidInsert_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + assertBookSize(side, 2 * maxDepth); + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, quoteId, InsertType.ADD_BACK, quoteId, price, size); + assertExchangeBookSize(COINBASE, side, 0); + assertBookSize(side, maxDepth); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementalUpdate_Cancel_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + + simulateCancel(COINBASE, quoteId); + assertExchangeBookSize(COINBASE, side, maxDepth - 1); + + simulateCancel(BINANCE, quoteId); + assertExchangeBookSize(BINANCE, side, maxDepth - 1); + + assertBookSize(side, 2 * maxDepth - 2); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementalUpdate_CancelTwice_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + simulateCancel(COINBASE, quoteId); + assertBookSize(side, 2 * maxDepth - 1); + simulateCancel(COINBASE, quoteId); + assertBookSize(side, maxDepth); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementalUpdate_Modify_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + + final int totalDepth = maxDepth * 2; + + simulateModify(COINBASE, quoteId, side, 1, price); + assertExchangeBookSize(BINANCE, side, maxDepth); + assertBookSize(side, totalDepth); + assertEqualPosition(side, getQuotePositionById(side, quoteId), Decimal64Utils.fromLong(price), 1, quoteId); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementalUpdate_Replace_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + + final int totalDepth = maxDepth * 2; + + final long pos = side == QuoteSide.BID ? bbo - price : price - bbo; + assertEqualPosition(side, (short) (2 * pos), Decimal64Utils.fromLong(price), size, quoteId); + Assertions.assertEquals(getQuoteByPosition(side, (int) (2 * pos)).getExchangeId(), COINBASE); + simulateReplace(COINBASE, quoteId, side, price, size); + assertEqualPosition(side, (short) (2 * pos + 1), Decimal64Utils.fromLong(price), size, quoteId); + Assertions.assertEquals(getQuoteByPosition(side, (int) (2 * pos + 1)).getExchangeId(), COINBASE); // lost priority + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + + final QuoteSide newSide = side == QuoteSide.ASK ? QuoteSide.BID : QuoteSide.ASK; + simulateReplace(BINANCE, quoteId, newSide, bbo, size); + assertExchangeBookSize(BINANCE, side, maxDepth - 1); + assertBookSize(newSide, totalDepth + 1); + assertEqualPosition(newSide, (short) 2, Decimal64Utils.fromLong(bbo), size, quoteId); // pos is now 3rd, 2 were there since snapshot + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementalUpdate_invalidReplace_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + + final int totalDepth = maxDepth * 2; + + final QuoteSide newSide = side == QuoteSide.ASK ? QuoteSide.BID : QuoteSide.ASK; + simulateReplace(BINANCE, quoteId, newSide, bbo, size); + assertExchangeBookSize(BINANCE, side, maxDepth - 1); + assertBookSize(newSide, totalDepth + 1); + assertEqualPosition(newSide, (short) 2, Decimal64Utils.fromLong(bbo), size, quoteId); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementalUpdate_invalidModify(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + assertBookSize(side, 2 * maxDepth); + simulateModify(COINBASE, quoteId, side, size + 10, price); + assertBookSize(side, maxDepth); + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, 2 * maxDepth); + simulateModify(BINANCE, quoteId, side, size, price - 1); + assertBookSize(side, maxDepth); + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, BINANCE, maxDepth, bbo, size); + assertBookSize(side, 2 * maxDepth); + simulateModify(COINBASE, quoteId, side, size - 100, price); + assertBookSize(side, maxDepth); + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, + mode = EnumSource.Mode.INCLUDE, + names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) + public void snapshot_L3Quote(final PackageType packageType) { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(packageType, BINANCE, maxDepth, bbo, size); + + assertBookSize(QuoteSide.BID, maxDepth * 2); + assertBookSize(QuoteSide.ASK, maxDepth * 2); + + assertIteratorBookQuotes(QuoteSide.ASK, 2 * maxDepth, bbo, size, 2); + assertIteratorBookQuotes(QuoteSide.BID, 2 * maxDepth, bbo, size, 2); + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, + mode = EnumSource.Mode.INCLUDE, + names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) + public void resetEntry_L3Quote(final PackageType packageType) { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + simulateQuoteSnapshot(packageType, BINANCE, maxDepth, bbo, size); + + simulateResetEntry(COINBASE, packageType); + assertIteratorBookQuotes(QuoteSide.ASK, maxDepth, bbo, size); + assertIteratorBookQuotes(QuoteSide.BID, maxDepth, bbo, size); + + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + + assertExchangeBookSize(COINBASE, QuoteSide.ASK, 0); + assertExchangeBookSize(COINBASE, QuoteSide.BID, 0); + + simulateResetEntry(BINANCE, packageType); + + assertExchangeBookSize(BINANCE, QuoteSide.ASK, 0); + assertExchangeBookSize(BINANCE, QuoteSide.BID, 0); + + assertBookSize(QuoteSide.BID, 0); + assertBookSize(QuoteSide.ASK, 0); + + Assertions.assertTrue(book.isEmpty()); + } +} diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L3SingleExchangeOrderBookTest.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L3SingleExchangeOrderBookTest.java new file mode 100644 index 0000000..0482d8d --- /dev/null +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/L3SingleExchangeOrderBookTest.java @@ -0,0 +1,684 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core; + +import com.epam.deltix.containers.CharSequenceUtils; +import com.epam.deltix.dfp.Decimal64Utils; +import com.epam.deltix.orderbook.core.api.*; +import com.epam.deltix.orderbook.core.fwk.AbstractL3QuoteLevelTest; +import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.timebase.messages.universal.*; +import com.epam.deltix.util.collections.generated.ObjectArrayList; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +import static com.epam.deltix.timebase.messages.universal.PackageType.INCREMENTAL_UPDATE; +import static com.epam.deltix.timebase.messages.universal.PackageType.VENDOR_SNAPSHOT; +import static com.epam.deltix.timebase.messages.universal.QuoteSide.ASK; +import static com.epam.deltix.timebase.messages.universal.QuoteSide.BID; +import static org.mockito.Mockito.*; + +/** + * @author Andrii_Ostapenko1 + */ +public class L3SingleExchangeOrderBookTest extends AbstractL3QuoteLevelTest { + + public BindOrderBookOptionsBuilder opt = new OrderBookOptionsBuilder() + .symbol(DEFAULT_SYMBOL) + .orderBookType(OrderBookType.SINGLE_EXCHANGE) + .quoteLevels(DataModelType.LEVEL_THREE) + .initialDepth(10) + .updateMode(UpdateMode.WAITING_FOR_SNAPSHOT); + + private OrderBook book = OrderBookFactory.create(opt.build()); + + @Override + public OrderBook getBook() { + return book; + } + + @Override + public void createBook(final OrderBookOptions otherOpt) { + opt.parent(otherOpt); + book = OrderBookFactory.create(opt.build()); + } + + private PackageHeader buildPackageHeaderSnapshot() { + final PackageHeader message = new PackageHeader(); + message.setPackageType(VENDOR_SNAPSHOT); + message.setEntries(new ObjectArrayList<>()); + message.setSymbol(DEFAULT_SYMBOL); +// message.setInstrumentType(InstrumentType.FX); + message.setTimeStampMs(System.currentTimeMillis()); + return message; + } + + private PackageHeader buildPackageHeaderIncrement() { + final PackageHeader message = new PackageHeader(); + message.setPackageType(INCREMENTAL_UPDATE); + message.setEntries(new ObjectArrayList<>()); + message.setSymbol(DEFAULT_SYMBOL); +// message.setInstrumentType(InstrumentType.FX); + message.setTimeStampMs(System.currentTimeMillis()); + return message; + } + + public void insert(final PackageHeader msg, + final QuoteSide side, + final int quantity, + final String price, + final String quoteId) { + final L3EntryNew entry = new L3EntryNew(); + entry.setInsertType(InsertType.ADD_BACK); + entry.setSide(side); + entry.setSize(Decimal64Utils.fromInt(quantity)); + entry.setPrice(Decimal64Utils.parse(price)); + entry.setQuoteId(quoteId); + entry.setExchangeId(COINBASE); + entry.setParticipantId(null); + msg.getEntries().add(entry); + } + + public void update(final PackageHeader msg, + final QuoteSide side, + final int quantity, + final String price, + final String quoteId, + final QuoteUpdateAction action) { + final L3EntryUpdate entry = new L3EntryUpdate(); + entry.setAction(action); + entry.setSide(side); + entry.setSize(Decimal64Utils.fromInt(quantity)); + entry.setPrice(Decimal64Utils.parse(price)); + entry.setQuoteId(quoteId); + entry.setExchangeId(COINBASE); + entry.setParticipantId(null); + msg.getEntries().add(entry); + } + + @Test + public void simple() { + final OrderBookOptions opt = new OrderBookOptionsBuilder().build(); + createBook(opt); + final PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 1, "99.99", "B2"); + insert(packageHeader, BID, 1, "78.99898", "B1"); + insert(packageHeader, ASK, 1, "100.001", "A1"); + book.update(packageHeader); + + Assertions.assertFalse(book.isEmpty()); + + final MarketSide bids = book.getMarketSide(BID); + Assertions.assertEquals(2, bids.depth()); + Assertions.assertTrue(bids.iterator().hasNext(), "iterator is not empty"); + + final OrderBookQuote bestBid = bids.iterator().next(); + Assertions.assertTrue(CharSequenceUtils.equals("B2", bestBid.getQuoteId())); + } + + @Test + @DisplayName("incremental insert: insert of new best quote") + public void simple_incremental_insert() { + final OrderBookOptions opt = new OrderBookOptionsBuilder().build(); + createBook(opt); + final MarketSide bids = book.getMarketSide(BID); + final MarketSide asks = book.getMarketSide(ASK); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 1, "78.99898", "B1"); + book.update(packageHeader); + + final int initialDepth = bids.depth() + asks.depth(); + + Assertions.assertEquals(1, bids.depth()); + Assertions.assertTrue(asks.isEmpty()); + + packageHeader = buildPackageHeaderIncrement(); + insert(packageHeader, BID, 1, "99.99", "B2"); + book.update(packageHeader); + + Assertions.assertEquals(initialDepth + 1, bids.depth()); + Assertions.assertTrue(CharSequenceUtils.equals("B2", bids.getBestQuote().getQuoteId())); + } + + @Test + @DisplayName("incremental update: replace quote of book with depth 1") + public void simple_incremental_update() { + final OrderBookOptions opt = new OrderBookOptionsBuilder().maxDepth(1).build(); + createBook(opt); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 1, "99.99", "B2"); + insert(packageHeader, ASK, 1, "78.99898", "A1"); + book.update(packageHeader); + + final MarketSide asks = book.getMarketSide(ASK); + Assertions.assertTrue(CharSequenceUtils.equals("A1", asks.getBestQuote().getQuoteId())); + + packageHeader = buildPackageHeaderIncrement(); + update(packageHeader, ASK, 1, "78.998", "B2", QuoteUpdateAction.REPLACE); + book.update(packageHeader); + + Assertions.assertEquals(1, asks.depth()); + Assertions.assertTrue(CharSequenceUtils.equals("B2", asks.getBestQuote().getQuoteId())); + } + + @Test + @DisplayName("Examples from") + public void example1() { + final OrderBookOptions opt = new OrderBookOptionsBuilder().build(); + createBook(opt); + final MarketSide bids = book.getMarketSide(BID); + final MarketSide asks = book.getMarketSide(ASK); + + Assertions.assertTrue(book.isEmpty()); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, ASK, 1, "10.15", "id0"); + insert(packageHeader, ASK, 1, "10.15", "id1"); + insert(packageHeader, ASK, 1, "10.15", "id2"); + insert(packageHeader, ASK, 1, "10.2", "id3"); + insert(packageHeader, ASK, 1, "10.2", "id4"); + book.update(packageHeader); + + Assertions.assertEquals(packageHeader.getEntries().size(), asks.depth()); + Assertions.assertEquals(0, bids.depth()); + + packageHeader = buildPackageHeaderIncrement(); + insert(packageHeader, ASK, 1, "10.15", "id6"); + book.update(packageHeader); + + final int curDepth = bids.depth() + asks.depth(); + Assertions.assertEquals(6, curDepth); + System.out.println(Decimal64Utils.toDouble(Decimal64Utils.parse("10.15"))); + assertEqualPosition(ASK, (short) 3, Decimal64Utils.parse("10.15"), 1, "id6"); + } + + @Test + @DisplayName("Examples from") + public void examples5_10() { + final OrderBookOptions opt = new OrderBookOptionsBuilder().build(); + createBook(opt); + + Assertions.assertTrue(book.isEmpty()); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 100, "10.15", "id0"); + insert(packageHeader, ASK, 20, "10.2", "id1"); + insert(packageHeader, BID, 20, "10.15", "id2"); + insert(packageHeader, ASK, 40, "10.2", "id3"); + insert(packageHeader, BID, 30, "10.15", "id4"); + insert(packageHeader, BID, 40, "10.1", "id5"); + insert(packageHeader, ASK, 50, "10.25", "id6"); + insert(packageHeader, BID, 2, "10.1", "id7"); + insert(packageHeader, ASK, 100, "10.25", "id8"); + insert(packageHeader, BID, 20, "10.05", "id9"); + insert(packageHeader, ASK, 50, "10.35", "id10"); + insert(packageHeader, BID, 20, "10.00", "id11"); + insert(packageHeader, ASK, 50, "10.35", "id12"); + insert(packageHeader, ASK, 20, "10.35", "id13"); + insert(packageHeader, BID, 90, "9.95", "id14"); + insert(packageHeader, ASK, 20, "10.4", "id15"); + insert(packageHeader, BID, 90, "9.95", "id16"); + + Assertions.assertTrue(book.update(packageHeader)); + + packageHeader = buildPackageHeaderIncrement(); + update(packageHeader, ASK, 40, "10.25", "id6", QuoteUpdateAction.MODIFY); + Assertions.assertTrue(book.update(packageHeader)); + assertEqualPosition(ASK, (short) 2, Decimal64Utils.parse("10.25"), 40, "id6"); + + packageHeader = buildPackageHeaderIncrement(); + update(packageHeader, ASK, 30, "10.25", "id6", QuoteUpdateAction.REPLACE); + Assertions.assertTrue(book.update(packageHeader)); + assertEqualPosition(ASK, (short) 2, Decimal64Utils.parse("10.25"), 100, "id8"); + assertEqualPosition(ASK, (short) 3, Decimal64Utils.parse("10.25"), 30, "id6"); + + packageHeader = buildPackageHeaderIncrement(); + update(packageHeader, ASK, 40, "10.25", "id5", QuoteUpdateAction.CANCEL); + Assertions.assertTrue(book.update(packageHeader)); + assertEqualPosition(BID, (short) 3, Decimal64Utils.parse("10.1"), 2, "id7"); + +// System.out.println("Example 8:"); +// packageHeader = increment() +// update(packageHeader, BID, 30, "10.12", "id4", QuoteUpdateAction.MODIFY) +// .build(); +// Assertions.assertFalse(book.update(packageHeader)); + + packageHeader = buildPackageHeaderIncrement(); + update(packageHeader, BID, 30, "10.12", "id4", QuoteUpdateAction.REPLACE); + Assertions.assertTrue(book.update(packageHeader)); + assertEqualPosition(BID, (short) 2, Decimal64Utils.parse("10.12"), 30, "id4"); + assertEqualPosition(BID, (short) 3, Decimal64Utils.parse("10.1"), 2, "id7"); + + packageHeader = buildPackageHeaderIncrement(); + update(packageHeader, BID, 80, "10.25", "id8", QuoteUpdateAction.REPLACE); + Assertions.assertTrue(book.update(packageHeader)); + assertEqualPosition(BID, (short) 0, Decimal64Utils.parse("10.25"), 80, "id8"); + + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should add new quote in order book") + public void snapshot_of_size_1(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + final PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, side, (int) size, Long.toString(price), quoteId.toString()); + getBook().update(packageHeader); + assertBookSize(side, 1); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should add new quote in order book") + public void incrementalUpdate_Insert(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, "id" + maxDepth, InsertType.ADD_BACK, quoteId, price, size); + assertBookSize(side, maxDepth + 1); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should not add new quote in order book") + public void incrementalUpdate_invalidInsert(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, quoteId, InsertType.ADD_BACK, quoteId, price, size); + assertBookSize(side, 0); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should cancel quote in order book") + public void incrementalUpdate_Cancel(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, price, size); + simulateCancel(COINBASE, quoteId); + assertBookSize(side, maxDepth - 1); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should fail to cancel the same quote twice") + public void incrementalUpdate_CancelTwice(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateCancel(COINBASE, quoteId); + assertBookSize(side, maxDepth - 1); + simulateCancel(COINBASE, quoteId); + assertBookSize(side, 0); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should replace quote and lose its priority") + public void incrementalUpdate_Replace(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, "id" + 2 * maxDepth, InsertType.ADD_BACK, null, price, size); + assertBookSize(side, maxDepth + 1); // now there are two quotes with same price + final short oldIdx = getQuotePositionById(side, quoteId); + simulateReplace(COINBASE, quoteId, side, price, size); + final short newIdx = getQuotePositionById(side, quoteId); + Assertions.assertNotEquals(oldIdx, newIdx); + assertEqualPosition(side, oldIdx, Decimal64Utils.fromLong(price), size, "id" + 2 * maxDepth); // new quote took its place + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should modify quote in order book and preserve its priority") + public void incrementalUpdate_Modify(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + + simulateModify(COINBASE, quoteId, side, size - 1, price); + assertBookSize(side, maxDepth); + assertEqualPosition(side, getQuotePositionById(side, quoteId), Decimal64Utils.fromLong(price), size - 1, quoteId); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should not modify quote in order book") + public void incrementalUpdate_invalidModify(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + simulateModify(COINBASE, quoteId, side, size + 10, price); + assertBookSize(side, 0); + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + simulateModify(COINBASE, quoteId, side, size, price - 1); + assertBookSize(side, 0); + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + simulateModify(COINBASE, quoteId, side, size - 100, price); + assertBookSize(side, 0); + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, mode = EnumSource.Mode.INCLUDE, names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) + public void snapshot(final PackageType packageType) { + int maxDepth = 10; + final int bbo = 25; + final int size = 5; + + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + assertBookSize(BID, maxDepth); + assertBookSize(ASK, maxDepth); + + maxDepth = maxDepth - 4; + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + assertBookSize(BID, maxDepth); + assertBookSize(ASK, maxDepth); + + maxDepth = maxDepth + 4; + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + assertBookSize(BID, maxDepth); + assertBookSize(ASK, maxDepth); + + for (int i = 1; i < 10; i++) { + maxDepth = i; + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + assertBookSize(BID, maxDepth); + assertBookSize(ASK, maxDepth); + } + + for (int i = 9; i > 0; i--) { + maxDepth = i; + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + assertBookSize(BID, maxDepth); + assertBookSize(ASK, maxDepth); + } + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, mode = EnumSource.Mode.INCLUDE, names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) + public void resetEntry(final PackageType packageType) { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + simulateResetEntry(COINBASE, packageType); + + assertBookSize(BID, 0); + assertBookSize(ASK, 0); + Assertions.assertTrue(book.isEmpty()); + } + + @ParameterizedTest + @EnumSource(value = QuoteSide.class, mode = EnumSource.Mode.INCLUDE) + public void iterateSide(final QuoteSide side) { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertIteratorBookQuotes(side, maxDepth, bbo, size); + } + + + @Test + @DisplayName("incremental insert: cancel of missing quote of new best quote") + public void simple_incremental_cancel() { + final ErrorListener errorListener = mock(ErrorListener.class); + final OrderBookOptions opt = new OrderBookOptionsBuilder().errorListener(errorListener).build(); + createBook(opt); + final MarketSide bids = book.getMarketSide(BID); + final MarketSide asks = book.getMarketSide(ASK); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 1, "78.99898", "B1"); + book.update(packageHeader); + + Assertions.assertEquals(1, bids.depth()); + Assertions.assertTrue(asks.isEmpty()); + + packageHeader = buildPackageHeaderIncrement(); + update(packageHeader, BID, 1, "99.99", "B3", QuoteUpdateAction.CANCEL); + book.update(packageHeader); + + verify(errorListener, times(1)).onError(packageHeader, EntryValidationCode.UNKNOWN_QUOTE_ID); + + } + + // Mockito Tests + @Test + @DisplayName("incremental insert: double insert of the same quoteId") + public void simple_incremental_mock_insert() { + final ErrorListener errorListener = mock(ErrorListener.class); + final OrderBookOptions opt = new OrderBookOptionsBuilder().errorListener(errorListener).build(); + createBook(opt); + final MarketSide bids = book.getMarketSide(BID); + final MarketSide asks = book.getMarketSide(ASK); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 1, "78.99898", "B1"); + book.update(packageHeader); + + Assertions.assertEquals(1, bids.depth()); + Assertions.assertTrue(asks.isEmpty()); + + packageHeader = buildPackageHeaderIncrement(); + insert(packageHeader, BID, 1, "99.99", "B1"); + book.update(packageHeader); + + verify(errorListener, times(1)).onError(packageHeader, EntryValidationCode.DUPLICATE_QUOTE_ID); + } + + @Test + @DisplayName("incremental insert: insert of the quote with negative size") + public void simple_incremental_insert_without_price() { + final ErrorListener errorListener = mock(ErrorListener.class); + final OrderBookOptions opt = new OrderBookOptionsBuilder().errorListener(errorListener).build(); + createBook(opt); + final MarketSide bids = book.getMarketSide(BID); + final MarketSide asks = book.getMarketSide(ASK); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 1, "78.99898", "B1"); + book.update(packageHeader); + + Assertions.assertEquals(1, bids.depth()); + Assertions.assertTrue(asks.isEmpty()); + + packageHeader = buildPackageHeaderIncrement(); + insert(packageHeader, BID, -123, "-123.3", "B3"); + book.update(packageHeader); + + verify(errorListener, times(1)).onError(packageHeader, EntryValidationCode.BAD_SIZE); + } + + @Test + @DisplayName("incremental modify: does not exist") + public void simple_incremental_modify() { + final ErrorListener errorListener = mock(ErrorListener.class); + final OrderBookOptions opt = new OrderBookOptionsBuilder().errorListener(errorListener).build(); + createBook(opt); + final MarketSide bids = book.getMarketSide(BID); + final MarketSide asks = book.getMarketSide(ASK); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 2, "78.99898", "B1"); + book.update(packageHeader); + + Assertions.assertEquals(1, bids.depth()); + Assertions.assertTrue(asks.isEmpty()); + + packageHeader = buildPackageHeaderIncrement(); + update(packageHeader, BID, 1, "78.99898", "B3", QuoteUpdateAction.MODIFY); + book.update(packageHeader); + + verify(errorListener, times(1)).onError(packageHeader, EntryValidationCode.UNKNOWN_QUOTE_ID); + } + + @Test + @DisplayName("incremental modify: price changed") + public void simple_incremental_modify_price() { + final ErrorListener errorListener = mock(ErrorListener.class); + final OrderBookOptions opt = new OrderBookOptionsBuilder().errorListener(errorListener).build(); + createBook(opt); + final MarketSide bids = book.getMarketSide(BID); + final MarketSide asks = book.getMarketSide(ASK); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 2, "78.99898", "B1"); + book.update(packageHeader); + + Assertions.assertEquals(1, bids.depth()); + Assertions.assertTrue(asks.isEmpty()); + + packageHeader = buildPackageHeaderIncrement(); + update(packageHeader, BID, 2, "78.99892", "B1", QuoteUpdateAction.MODIFY); + book.update(packageHeader); + + verify(errorListener, times(1)).onError(packageHeader, EntryValidationCode.MODIFY_CHANGE_PRICE); + } + + @Test + @DisplayName("incremental modify: size increased") + public void simple_incremental_modify_size() { + final ErrorListener errorListener = mock(ErrorListener.class); + final OrderBookOptions opt = new OrderBookOptionsBuilder().errorListener(errorListener).build(); + createBook(opt); + final MarketSide bids = book.getMarketSide(BID); + final MarketSide asks = book.getMarketSide(ASK); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 2, "78.99898", "B1"); + book.update(packageHeader); + + Assertions.assertEquals(1, bids.depth()); + Assertions.assertTrue(asks.isEmpty()); + + packageHeader = buildPackageHeaderIncrement(); + update(packageHeader, BID, 2, "78.99898", "B1", QuoteUpdateAction.MODIFY); + book.update(packageHeader); + + verify(errorListener, times(0)).onError(packageHeader, EntryValidationCode.MODIFY_INCREASE_SIZE); + + packageHeader = buildPackageHeaderIncrement(); + update(packageHeader, BID, 4, "78.99898", "B1", QuoteUpdateAction.MODIFY); + book.update(packageHeader); + + verify(errorListener, times(1)).onError(packageHeader, EntryValidationCode.MODIFY_INCREASE_SIZE); + } + + @Test + @DisplayName("incremental insert: exchanges differ") + public void simple_incremental_wrong_exchangeId() { + final ErrorListener errorListener = mock(ErrorListener.class); + final OrderBookOptions opt = new OrderBookOptionsBuilder().errorListener(errorListener).build(); + createBook(opt); + final MarketSide bids = book.getMarketSide(BID); + final MarketSide asks = book.getMarketSide(ASK); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 2, "78.99898", "B1"); + book.update(packageHeader); + + Assertions.assertEquals(1, bids.depth()); + Assertions.assertTrue(asks.isEmpty()); + + packageHeader = buildPackageHeaderIncrement(); + final L3EntryUpdate entry = new L3EntryUpdate(); + entry.setAction(QuoteUpdateAction.MODIFY); + entry.setSide(BID); + entry.setSize(Decimal64Utils.fromInt(1)); + entry.setPrice(Decimal64Utils.parse("78.99892")); + entry.setQuoteId("B1"); + entry.setExchangeId(BINANCE); + entry.setParticipantId(null); + packageHeader.getEntries().add(entry); + book.update(packageHeader); + + verify(errorListener, times(1)).onError(packageHeader, EntryValidationCode.EXCHANGE_ID_MISMATCH); + } + + @Test + @DisplayName("incremental insert: using action other than ADD_BACK") + public void simple_incremental_add_not_back() { + final ErrorListener errorListener = mock(ErrorListener.class); + final OrderBookOptions opt = new OrderBookOptionsBuilder().errorListener(errorListener).build(); + createBook(opt); + final MarketSide bids = book.getMarketSide(BID); + final MarketSide asks = book.getMarketSide(ASK); + + PackageHeader packageHeader = buildPackageHeaderSnapshot(); + insert(packageHeader, BID, 2, "78.99898", "B1"); + book.update(packageHeader); + + Assertions.assertEquals(1, bids.depth()); + Assertions.assertTrue(asks.isEmpty()); + + packageHeader = buildPackageHeaderIncrement(); + final L3EntryNew entry = new L3EntryNew(); + entry.setSide(BID); + entry.setSize(Decimal64Utils.fromInt(1)); + entry.setPrice(Decimal64Utils.parse("78.99892")); + entry.setQuoteId("B2"); + entry.setExchangeId(COINBASE); + entry.setParticipantId(null); + entry.setInsertType(InsertType.ADD_BEFORE); + entry.setInsertBeforeQuoteId("B1"); + packageHeader.getEntries().add(entry); + book.update(packageHeader); + verify(errorListener, times(1)).onError(packageHeader, EntryValidationCode.UNSUPPORTED_INSERT_TYPE); + } + +} diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractL1QuoteLevelTest.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractL1QuoteLevelTest.java index e600847..213ea2d 100644 --- a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractL1QuoteLevelTest.java +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractL1QuoteLevelTest.java @@ -18,9 +18,10 @@ import com.epam.deltix.dfp.Decimal; import com.epam.deltix.dfp.Decimal64Utils; -import com.epam.deltix.orderbook.core.options.OrderBookOptions; -import com.epam.deltix.orderbook.core.options.OrderBookOptionsBuilder; -import com.epam.deltix.orderbook.core.options.UpdateMode; + +import com.epam.deltix.orderbook.core.api.OrderBookQuoteTimestamp; +import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.timebase.messages.service.FeedStatus; import com.epam.deltix.timebase.messages.universal.PackageType; import com.epam.deltix.timebase.messages.universal.QuoteSide; import org.junit.jupiter.api.Assertions; @@ -28,6 +29,10 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; +import static com.epam.deltix.timebase.messages.universal.PackageType.PERIODICAL_SNAPSHOT; +import static com.epam.deltix.timebase.messages.universal.PackageType.VENDOR_SNAPSHOT; + + /** * @author Andrii_Ostapenko1 * @created 17/01/2022 - 10:12 PM @@ -101,6 +106,48 @@ public void snapshot_L1Quote_base_isEmpty(final int bbo, assertBookSize(side, 1); } + @ParameterizedTest + @MethodSource("quoteProvider") + public void shouldStoreQuoteTimestamp_L1Quote(final int bbo, + final QuoteSide side, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .shouldStoreQuoteTimestamps(true) + .build(); + createBook(opt); + simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders); + getBook().getMarketSide(side) + .forEach(q -> { + Assertions.assertTrue(q.hasOriginalTimestamp()); + Assertions.assertTrue(q.getOriginalTimestamp() != OrderBookQuoteTimestamp.TIMESTAMP_UNKNOWN); + Assertions.assertTrue(q.hasTimestamp()); + Assertions.assertTrue(q.getTimestamp() != OrderBookQuoteTimestamp.TIMESTAMP_UNKNOWN); + }); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void shouldNotStoreQuoteTimestamp_L1Quote(final int bbo, + final QuoteSide side, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .shouldStoreQuoteTimestamps(false) + .build(); + createBook(opt); + simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders); + getBook().getMarketSide(side) + .forEach(q -> { + Assertions.assertFalse(q.hasOriginalTimestamp()); + Assertions.assertEquals(OrderBookQuoteTimestamp.TIMESTAMP_UNKNOWN, q.getOriginalTimestamp()); + Assertions.assertFalse(q.hasTimestamp()); + Assertions.assertEquals(OrderBookQuoteTimestamp.TIMESTAMP_UNKNOWN, q.getTimestamp()); + }); + } + @ParameterizedTest @MethodSource("quoteProvider") public void incrementalUpdate_Insert_L1Quote_invalidSymbol(final int bbo, @@ -109,8 +156,139 @@ public void incrementalUpdate_Insert_L1Quote_invalidSymbol(final int bbo, @Decimal final long size, final long numberOfOrders) { Assertions.assertFalse(simulateL1Insert(LTC_SYMBOL, BINANCE, side, price, size, numberOfOrders)); - simulateL1QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders); + simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders); Assertions.assertFalse(simulateL1Insert(LTC_SYMBOL, BINANCE, side, price, size, numberOfOrders)); } + @ParameterizedTest + @MethodSource("quoteProvider") + public void securityStatusMessage_L2Quote(final int bbo, + final QuoteSide side, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders); + + simulateSecurityFeedStatus(COINBASE, FeedStatus.NOT_AVAILABLE); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure book is clean + + simulateL1Insert(DEFAULT_SYMBOL, COINBASE, side, price, size, numberOfOrders); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure insert is ignored (we are waiting for snapshot) + + simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders); + assertBookSize(side, 1); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_L1Quote_NoWaitingSnapshot(final int bbo, + final QuoteSide side, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .resetMode(ResetMode.NON_WAITING_FOR_SNAPSHOT) + .build(); + createBook(opt); + + simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders); + + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure book is clean + + simulateL1Insert(DEFAULT_SYMBOL, COINBASE, side, price, size, numberOfOrders); + assertExchangeBookSize(COINBASE, side, 1); // make sure insert is not ignored + + simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders); + assertBookSize(side, 1); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_L1Quote_WaitingSnapshot(final int bbo, + final QuoteSide side, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .resetMode(ResetMode.WAITING_FOR_SNAPSHOT) + .build(); + createBook(opt); + + simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders); + + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure book is clean + + simulateL1Insert(DEFAULT_SYMBOL, COINBASE, side, price, size, numberOfOrders); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure insert is ignored + + simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders); + assertBookSize(side, 1); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_L1Quote_PeriodicalSnapshotMode_ONLY_ONE(final int bbo, + final QuoteSide side, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .periodicalSnapshotMode(PeriodicalSnapshotMode.ONLY_ONE) + .build(); + createBook(opt); + + Assertions.assertTrue(simulateL1QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + assertBookSize(side, 1); + Assertions.assertFalse(simulateL1QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + Assertions.assertTrue(simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + Assertions.assertFalse(simulateL1QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + simulateL1Insert(DEFAULT_SYMBOL, COINBASE, side, price, size, numberOfOrders); + Assertions.assertTrue(simulateL1QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_L1Quote_PeriodicalSnapshotMode_PROCESS_ALL(final int bbo, + final QuoteSide side, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .periodicalSnapshotMode(PeriodicalSnapshotMode.PROCESS_ALL) + .build(); + createBook(opt); + + Assertions.assertTrue(simulateL1QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + Assertions.assertTrue(simulateL1QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + Assertions.assertTrue(simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + Assertions.assertTrue(simulateL1QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_L1Quote_PeriodicalSnapshotMode_SKIP_ALL(final int bbo, + final QuoteSide side, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .periodicalSnapshotMode(PeriodicalSnapshotMode.SKIP_ALL) + .build(); + createBook(opt); + + Assertions.assertFalse(simulateL1QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + Assertions.assertFalse(simulateL1QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + Assertions.assertTrue(simulateL1QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + Assertions.assertFalse(simulateL1QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + simulateL1Insert(DEFAULT_SYMBOL, COINBASE, side, price, size, numberOfOrders); + Assertions.assertFalse(simulateL1QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, bbo, size, numberOfOrders)); + } + } diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractL2QuoteLevelTest.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractL2QuoteLevelTest.java index d218697..3894f91 100644 --- a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractL2QuoteLevelTest.java +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractL2QuoteLevelTest.java @@ -18,10 +18,11 @@ import com.epam.deltix.dfp.Decimal; import com.epam.deltix.dfp.Decimal64Utils; -import com.epam.deltix.orderbook.core.options.GapMode; -import com.epam.deltix.orderbook.core.options.OrderBookOptions; -import com.epam.deltix.orderbook.core.options.OrderBookOptionsBuilder; -import com.epam.deltix.orderbook.core.options.UnreachableDepthMode; + +import com.epam.deltix.orderbook.core.api.OrderBookQuoteTimestamp; +import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.timebase.messages.TypeConstants; +import com.epam.deltix.timebase.messages.service.FeedStatus; import com.epam.deltix.timebase.messages.universal.PackageType; import com.epam.deltix.timebase.messages.universal.QuoteSide; import org.junit.jupiter.api.Assertions; @@ -33,8 +34,12 @@ import java.util.stream.Stream; +import static com.epam.deltix.timebase.messages.universal.PackageType.PERIODICAL_SNAPSHOT; import static com.epam.deltix.timebase.messages.universal.PackageType.VENDOR_SNAPSHOT; + + + /** * @author Andrii_Ostapenko1 * @created 17/01/2022 - 10:12 PM @@ -44,18 +49,72 @@ public abstract class AbstractL2QuoteLevelTest extends AbstractOrderBookTest { public static Stream packageTypeAndSideProvider() { return Stream.of( Arguments.of(VENDOR_SNAPSHOT, QuoteSide.ASK), - Arguments.of(PackageType.PERIODICAL_SNAPSHOT, QuoteSide.ASK), + Arguments.of(PERIODICAL_SNAPSHOT, QuoteSide.ASK), Arguments.of(VENDOR_SNAPSHOT, QuoteSide.BID), - Arguments.of(PackageType.PERIODICAL_SNAPSHOT, QuoteSide.BID) + Arguments.of(PERIODICAL_SNAPSHOT, QuoteSide.BID) + ); + } + + public static Stream sideProvider() { + return Stream.of( + Arguments.of(QuoteSide.ASK), + Arguments.of(QuoteSide.BID) ); } + @ParameterizedTest + @MethodSource("quoteProvider") + public void shouldStoreQuoteTimestamp_L1Quote(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final int priceLevel, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .shouldStoreQuoteTimestamps(true) + .build(); + createBook(opt); + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); + getBook().getMarketSide(side) + .forEach(q -> { + Assertions.assertTrue(q.hasOriginalTimestamp()); + Assertions.assertTrue(q.getOriginalTimestamp() != OrderBookQuoteTimestamp.TIMESTAMP_UNKNOWN); + Assertions.assertTrue(q.hasTimestamp()); + Assertions.assertTrue(q.getTimestamp() != OrderBookQuoteTimestamp.TIMESTAMP_UNKNOWN); + }); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void shouldNotStoreQuoteTimestamp_L1Quote(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final int priceLevel, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders, + final boolean addStatistics) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .shouldStoreQuoteTimestamps(false) + .build(); + createBook(opt); + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + getBook().getMarketSide(side) + .forEach(q -> { + Assertions.assertFalse(q.hasOriginalTimestamp()); + Assertions.assertEquals(OrderBookQuoteTimestamp.TIMESTAMP_UNKNOWN, q.getOriginalTimestamp()); + Assertions.assertFalse(q.hasTimestamp()); + Assertions.assertEquals(OrderBookQuoteTimestamp.TIMESTAMP_UNKNOWN, q.getTimestamp()); + }); + } + @ParameterizedTest @MethodSource("quoteProvider") public void incrementalUpdate_Insert_L1Quote_invalidPackage(final int maxExchangeDepth, final int bbo, final QuoteSide side, - final short priceLevel, + final int priceLevel, @Decimal final long price, @Decimal final long size, final long numberOfOrders) { @@ -69,7 +128,7 @@ public void incrementalUpdate_Insert_L1Quote_invalidPackage(final int maxExchang public void incrementalUpdate_Insert_L2Quote_invalidSymbol(final int maxExchangeDepth, final int bbo, final QuoteSide side, - final short priceLevel, + final int priceLevel, @Decimal final long price, @Decimal final long size, final long numberOfOrders) { @@ -83,7 +142,7 @@ public void incrementalUpdate_Insert_L2Quote_invalidSymbol(final int maxExchange public void incrementalUpdate_Delete_base_L2Quote(final int maxExchangeDepth, final int bbo, final QuoteSide side, - final short priceLevel, + final int priceLevel, @Decimal final long price, @Decimal final long size, final long numberOfOrders) { @@ -96,7 +155,7 @@ public void incrementalUpdate_Delete_base_L2Quote(final int maxExchangeDepth, public void incrementUpdate_Insert_base_L2Quote(final int maxExchangeDepth, final int bbo, final QuoteSide side, - final short priceLevel, + final int priceLevel, @Decimal final long price, @Decimal final long size, final long numberOfOrders) { @@ -109,7 +168,7 @@ public void incrementUpdate_Insert_base_L2Quote(final int maxExchangeDepth, public void incrementUpdate_Update_base_L2Quote(final int maxExchangeDepth, final int bbo, final QuoteSide side, - final short priceLevel, + final int priceLevel, @Decimal final long price, @Decimal final long size, final long numberOfOrders) { @@ -121,6 +180,41 @@ public void incrementUpdate_Update_base_L2Quote(final int maxExchangeDepth, @EnumSource(value = PackageType.class, mode = EnumSource.Mode.INCLUDE, names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) + public void l2Quote_bbo_quote(final PackageType packageType) { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + final int numberOfOrders = 25; + + Assertions.assertNull(getBook().getMarketSide(QuoteSide.BID).getBestQuote()); + Assertions.assertNull(getBook().getMarketSide(QuoteSide.BID).getWorstQuote()); + + Assertions.assertNull(getBook().getMarketSide(QuoteSide.ASK).getBestQuote()); + Assertions.assertNull(getBook().getMarketSide(QuoteSide.ASK).getWorstQuote()); + + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); + + final int expectedWorstBid = bbo - maxDepth + 1; + final int expectedWorstAsk = bbo + maxDepth - 1; + + Assertions.assertEquals(expectedWorstBid, Decimal64Utils.toInt(getBook().getMarketSide(QuoteSide.BID).getWorstQuote().getPrice())); + Assertions.assertEquals(bbo, Decimal64Utils.toInt(getBook().getMarketSide(QuoteSide.BID).getBestQuote().getPrice())); + Assertions.assertEquals(bbo, Decimal64Utils.toInt(getBook().getMarketSide(QuoteSide.ASK).getBestQuote().getPrice())); + Assertions.assertEquals(expectedWorstAsk, Decimal64Utils.toInt(getBook().getMarketSide(QuoteSide.ASK).getWorstQuote().getPrice())); + + getBook().clear(); + + Assertions.assertNull(getBook().getMarketSide(QuoteSide.BID).getBestQuote()); + Assertions.assertNull(getBook().getMarketSide(QuoteSide.BID).getWorstQuote()); + + Assertions.assertNull(getBook().getMarketSide(QuoteSide.ASK).getBestQuote()); + Assertions.assertNull(getBook().getMarketSide(QuoteSide.ASK).getWorstQuote()); + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, + mode = EnumSource.Mode.INCLUDE, + names = {"VENDOR_SNAPSHOT"}) public void snapshot_L2Quote_base_clear(final PackageType packageType) { final int maxDepth = 10; final int bbo = 25; @@ -163,27 +257,45 @@ public void snapshot_L2Quote_base_totalQuantity(final PackageType packageType) { names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) public void incrementalUpdate_L2Quote_addLevelMoreThenSnapshotDepth(final PackageType packageType) { final int maxDepth = 10; - final int bbo = 25; + final int bbo = 250; final int size = 5; final int numberOfOrders = 25; + assertBookSize(QuoteSide.BID, 0); + assertBookSize(QuoteSide.ASK, 0); + //TODO refactor (make more simple) simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + for (int i = 0; i < maxDepth; i++) { simulateL2Insert(COINBASE, QuoteSide.ASK, - (short) ((short) maxDepth + i), - Decimal64Utils.fromInt(80 + i), - Decimal64Utils.fromInt(size), numberOfOrders); + maxDepth + i, + bbo + maxDepth + i, + size, numberOfOrders); assertBookSize(QuoteSide.ASK, maxDepth + i + 1); } + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, 2 * maxDepth); + simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + assertPrice(QuoteSide.BID, 0, getExpectedQuotePrice(QuoteSide.BID, bbo, 0)); + assertPrice(QuoteSide.BID, 1, getExpectedQuotePrice(QuoteSide.BID, bbo, 1)); + assertPrice(QuoteSide.BID, 2, getExpectedQuotePrice(QuoteSide.BID, bbo, 2)); + assertPrice(QuoteSide.ASK, 0, getExpectedQuotePrice(QuoteSide.ASK, bbo, 0)); + assertPrice(QuoteSide.ASK, 1, getExpectedQuotePrice(QuoteSide.ASK, bbo, 1)); + assertPrice(QuoteSide.ASK, 2, getExpectedQuotePrice(QuoteSide.ASK, bbo, 2)); + for (int i = 0; i < maxDepth; i++) { simulateL2Insert(COINBASE, QuoteSide.BID, - (short) ((short) maxDepth + i), - Decimal64Utils.fromInt(80 - i), - Decimal64Utils.fromInt(size), numberOfOrders); + maxDepth + i, + bbo - maxDepth - i, + size, numberOfOrders); assertBookSize(QuoteSide.BID, maxDepth + i + 1); } } @@ -193,31 +305,35 @@ public void incrementalUpdate_L2Quote_addLevelMoreThenSnapshotDepth(final Packag public void incrementUpdate_RandomDelete_InsertLast_L2Quote(final int maxExchangeDepth, final int bbo, final QuoteSide side, - final short priceLevel, - @Decimal final long price, - @Decimal final long size, + final int priceLevel, + final long price, + final long size, final long numberOfOrders) { simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); assertExchangeBookSize(COINBASE, side, maxExchangeDepth); simulateL2Delete(COINBASE, side, priceLevel, price, size, numberOfOrders); assertExchangeBookSize(COINBASE, side, maxExchangeDepth - 1); - simulateL2Insert(COINBASE, side, (short) (maxExchangeDepth - 1), price, size, numberOfOrders); + if (side == QuoteSide.ASK) { + simulateL2Insert(COINBASE, side, maxExchangeDepth - 1, bbo + maxExchangeDepth + 1, size, numberOfOrders); + } else { + simulateL2Insert(COINBASE, side, maxExchangeDepth - 1, bbo - maxExchangeDepth + 1, size, numberOfOrders); + } assertExchangeBookSize(COINBASE, side, maxExchangeDepth); } @ParameterizedTest - @MethodSource("packageTypeAndSideProvider") - public void snapshot_L2Quote_snapshotAfterDelete(final PackageType packageType, - final QuoteSide side) { + @MethodSource("sideProvider") + public void snapshot_L2Quote_snapshotAfterDelete( + final QuoteSide side) { final int maxDepth = 10; final int bbo = 25; final int size = 5; final int numberOfOrders = 25; final int numberOfDeletedQuotes = 2; - simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); - simulateL2Delete(numberOfDeletedQuotes, COINBASE, side, (short) 0, 0, 0); - simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size, numberOfOrders); + simulateL2Delete(numberOfDeletedQuotes, COINBASE, side, 0, 0, 0); + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size, numberOfOrders); assertBookSize(side, maxDepth); } @@ -228,28 +344,6 @@ public void snapshot_L2Quote_base_isEmpty() { Assertions.assertTrue(getBook().isEmpty()); } - //TODO Refactor this test (make more simple) - @ParameterizedTest - @MethodSource("packageTypeAndSideProvider") - public void incrementUpdate_Insert_Skip_Gaps_Quote(final PackageType packageType, - final QuoteSide side) { - final int maxDepth = 10; - final int bbo = 25; - final int size = 5; - final int numberOfOrders = 25; - - final int numberOfDeletedQuotes = 4; - final int expectedDepth = maxDepth - numberOfDeletedQuotes; - - simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); - simulateL2Delete(numberOfDeletedQuotes, COINBASE, side, 0, 0, 0); - - for (int i = expectedDepth + 1; i < maxDepth + 1; i++) { - simulateL2Insert(COINBASE, side, (short) 8, Decimal64Utils.fromInt(80), Decimal64Utils.fromInt(size), numberOfOrders); - assertBookSize(side, expectedDepth); - } - } - //TODO Refactor this test (make more simple) @ParameterizedTest @MethodSource("packageTypeAndSideProvider") @@ -260,80 +354,46 @@ public void incrementUpdate_Update_Skip_Gaps_Quote(final PackageType packageType final int size = 5; final int numberOfOrders = 25; - final int numberOfDeletedQuotes = 2; - final int expectedDepth = maxDepth - numberOfDeletedQuotes; - - simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); - simulateL2Delete(numberOfDeletedQuotes, COINBASE, side, 0, 0, 0); - - for (int i = expectedDepth; i < maxDepth + 1; i++) { - simulateL2Update(COINBASE, side, (short) 8, Decimal64Utils.fromInt(80), Decimal64Utils.fromInt(size), numberOfOrders); - assertBookSize(side, expectedDepth); - } - } - - - // GAP MODE - - @ParameterizedTest - @EnumSource(value = PackageType.class, - mode = EnumSource.Mode.INCLUDE, - names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) - public void snapshot_maxDepth_gapModeSkip_L2Quote(final PackageType packageType) { - final int maxDepth = 10; - final int processDepth = 5; - final int bbo = 25; - final int size = 5; - final int numberOfOrders = 25; - - final OrderBookOptions opt = new OrderBookOptionsBuilder().maxDepth(processDepth).gapMode(GapMode.SKIP).build(); + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .validationOptions(ValidationOptions.builder() + .validateQuoteInsert() + .skipInvalidQuoteUpdate() + .build()) + .build(); createBook(opt); simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); - assertBookSizeBySides(processDepth); - } - - @ParameterizedTest - @MethodSource("quoteProvider") - public void snapshot_maxDepth_gapModeSkip_dynamic_insert_L2Quote(final int maxExchangeDepth, - final int bbo, - final QuoteSide side, - final short priceLevel, - @Decimal final long price, - @Decimal final long size, - final long numOfOrders) { - - final int maxDepth = priceLevel + 1; - final OrderBookOptions opt = new OrderBookOptionsBuilder().maxDepth(maxDepth).gapMode(GapMode.SKIP).build(); - createBook(opt); - - simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numOfOrders); - simulateL2Insert(COINBASE, side, priceLevel, price, size, numOfOrders); - assertBookSizeBySides(maxDepth); - assertEqualLevel(side, priceLevel, price, size, numOfOrders); + simulateL2Update(COINBASE, side, 0, Decimal64Utils.fromInt(0), Decimal64Utils.fromInt(size), numberOfOrders); + assertBookSize(side, maxDepth); } - @ParameterizedTest @MethodSource("quoteProvider") public void snapshot_maxDepth_unreachableDepthModeSkip_insert_L2Quote(final int maxExchangeDepth, final int bbo, final QuoteSide side, - final short priceLevel, - @Decimal final long price, - @Decimal final long size, + final int priceLevel, + final long price, + final long size, final long numOfOrders) { final int maxDepth = 2; final OrderBookOptions opt = new OrderBookOptionsBuilder() .maxDepth(maxDepth) - .unreachableDepthMode(UnreachableDepthMode.SKIP) + .validationOptions(ValidationOptions.builder() + .skipInvalidQuoteInsert() + .skipInvalidQuoteUpdate() + .build()) .build(); createBook(opt); simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numOfOrders); - simulateL2Insert(COINBASE, side, priceLevel, price, size, numOfOrders); + if (side == QuoteSide.ASK) { + simulateL2Insert(COINBASE, side, priceLevel, bbo + 1, size, numOfOrders); + } else { + simulateL2Insert(COINBASE, side, priceLevel, bbo - 1, size, numOfOrders); + } assertBookSizeBySides(maxDepth); } @@ -342,13 +402,18 @@ public void snapshot_maxDepth_unreachableDepthModeSkip_insert_L2Quote(final int public void snapshot_maxDepth_gapModeSkip_delete_L2Quote(final int maxExchangeDepth, final int bbo, final QuoteSide side, - final short priceLevel, + final int priceLevel, @Decimal final long price, @Decimal final long size, final long numOfOrders) { int maxDepth = 2; - final OrderBookOptions opt = new OrderBookOptionsBuilder().maxDepth(maxDepth).gapMode(GapMode.SKIP).build(); + final OrderBookOptions opt = new OrderBookOptionsBuilder().maxDepth(maxDepth) + .validationOptions(ValidationOptions.builder() + .validateQuoteInsert() + .skipInvalidQuoteUpdate() + .build()) + .build(); createBook(opt); simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numOfOrders); @@ -363,23 +428,38 @@ public void snapshot_maxDepth_gapModeSkip_delete_L2Quote(final int maxExchangeDe assertBookSize(side, maxDepth); } - @ParameterizedTest @EnumSource(value = PackageType.class, mode = EnumSource.Mode.INCLUDE, - names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) - public void snapshot_maxDepth_gapModeSkipAndDrop_L2Quote(final PackageType packageType) { - final int maxDepth = 10; - final int processDepth = 5; + names = {"VENDOR_SNAPSHOT"} + ) + // TODO: 11/28/2022 refactor.. Add optimization during update snapshot with same depth + public void snapshot_byOneSide(final PackageType packageType) { + final int maxDepth = 4; + final int processDepth = 4; final int bbo = 25; final int size = 5; final int numberOfOrders = 25; - final OrderBookOptions opt = new OrderBookOptionsBuilder().maxDepth(processDepth).gapMode(GapMode.SKIP_AND_DROP).build(); - createBook(opt); - +// Filling two side 4:4 simulateL2QuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size, numberOfOrders); assertBookSizeBySides(processDepth); +// Cleaning one side 0:4 + simulateL2DeleteAllQuoteBySide(QuoteSide.BID); + assertExchangeBookSizeIsEmpty(QuoteSide.BID); + assertBookSize(QuoteSide.ASK, maxDepth); +// Filling opposite side 4:0 + simulateL2QuoteSnapshotBySide(packageType, COINBASE, maxDepth, QuoteSide.BID, bbo, size, numberOfOrders, true); + assertBookSize(QuoteSide.BID, maxDepth); + assertExchangeBookSizeIsEmpty(QuoteSide.ASK); +// Filling two side 2:2 + simulateL2QuoteSnapshot(packageType, COINBASE, 2, bbo, size, numberOfOrders); + assertBookSize(QuoteSide.BID, 2); + assertBookSize(QuoteSide.ASK, 2); +// Filling two side 4:4 + simulateL2QuoteSnapshot(packageType, COINBASE, 4, bbo, size, numberOfOrders); + assertBookSize(QuoteSide.BID, 4); + assertBookSize(QuoteSide.ASK, 4); } @ParameterizedTest @@ -387,38 +467,245 @@ public void snapshot_maxDepth_gapModeSkipAndDrop_L2Quote(final PackageType packa public void snapshot_maxDepth_unreachableDepthMode_SkipAndDrop_insert_L2Quote(final int maxExchangeDepth, final int bbo, final QuoteSide side, - final short priceLevel, + final int priceLevel, @Decimal final long price, @Decimal final long size, final long numOfOrders) { final OrderBookOptions opt = new OrderBookOptionsBuilder() .maxDepth(priceLevel) - .unreachableDepthMode(UnreachableDepthMode.SKIP_AND_DROP) + .validationOptions(ValidationOptions.builder() + .validateQuoteInsert() + .validateQuoteUpdate() + .build()) .build(); createBook(opt); simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numOfOrders); - simulateL2Insert(COINBASE, side, (short) (priceLevel + 2), price, size, numOfOrders); + simulateL2Insert(COINBASE, side, priceLevel + 2, price, size, numOfOrders); assertBookSizeBySides(0); } @ParameterizedTest @MethodSource("quoteProvider") - public void snapshot_gapModeSkipAndDrop_insert_L2Quote(final int maxExchangeDepth, - final int bbo, - final QuoteSide side, - final short priceLevel, - @Decimal final long price, - @Decimal final long size, - final long numOfOrders) { + public void securityStatusMessage_L2Quote(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final int priceLevel, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders, + final boolean addStatistics) { + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + + simulateSecurityFeedStatus(COINBASE, FeedStatus.NOT_AVAILABLE); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure book is clean + + simulateL2Insert(COINBASE, side, priceLevel, price, size, numberOfOrders); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure insert is ignored (we are waiting for snapshot) + + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders, addStatistics); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_L2Quote_WaitingSnapshot(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final int priceLevel, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { final OrderBookOptions opt = new OrderBookOptionsBuilder() - .gapMode(GapMode.SKIP_AND_DROP) + .resetMode(ResetMode.WAITING_FOR_SNAPSHOT) .build(); createBook(opt); - simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numOfOrders); - simulateL2Insert(COINBASE, side, (short) (maxExchangeDepth + priceLevel + 1), price, size, numOfOrders); - assertBookSizeBySides(0); + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); + + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure book is clean + + simulateL2Insert(COINBASE, side, priceLevel, price, size, numberOfOrders); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure insert is ignored (we are waiting for snapshot) + + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_L2Quote_PeriodicalSnapshotMode_ONLY_ONE(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final int priceLevel, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .periodicalSnapshotMode(PeriodicalSnapshotMode.ONLY_ONE) + .build(); + createBook(opt); + + simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure book is clean + + simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + + double updatePrice = price; + if (side == QuoteSide.ASK) { + updatePrice = updatePrice - 0.5; + } else { + updatePrice = updatePrice + 0.5; + } + + simulateL2Insert(COINBASE, side, priceLevel, updatePrice, size, numberOfOrders); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth + 1); // make sure insert is processed + + Assertions.assertFalse(simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth + 1); // make sure book is ignore periodical snapshot after insert + + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + Assertions.assertTrue(simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + Assertions.assertFalse(simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + } + + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_L2Quote_PeriodicalSnapshotMode_SKIP_ALL(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final int priceLevel, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .periodicalSnapshotMode(PeriodicalSnapshotMode.SKIP_ALL) + .build(); + createBook(opt); + + Assertions.assertFalse(simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + Assertions.assertTrue(simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + + Assertions.assertFalse(simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + Assertions.assertFalse(simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure book is clean + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_L2Quote_PeriodicalSnapshotMode_PROCESS_ALL(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final int priceLevel, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .periodicalSnapshotMode(PeriodicalSnapshotMode.PROCESS_ALL) + .build(); + createBook(opt); + + Assertions.assertTrue(simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + Assertions.assertTrue(simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + + Assertions.assertTrue(simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + Assertions.assertTrue(simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + Assertions.assertTrue(simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + Assertions.assertTrue(simulateL2QuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders)); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + } + + @Test + public void resetEntry() { + simulateResetEntry(DEFAULT_EXCHANGE_ID, VENDOR_SNAPSHOT); + assertExchangeBookSizeIsEmpty(QuoteSide.BID); + assertExchangeBookSizeIsEmpty(QuoteSide.ASK); + Assertions.assertFalse(getBook().getExchanges().isEmpty()); + } + + @Test + public void resetEntry_Invalid() { + Assertions.assertFalse(simulateResetEntry(TypeConstants.EXCHANGE_NULL, VENDOR_SNAPSHOT)); + Assertions.assertTrue(getBook().isEmpty()); + Assertions.assertTrue(getBook().getExchanges().isEmpty()); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_L2Quote_NoWaitingSnapshot(final int maxExchangeDepth, + final int bbo, + final QuoteSide side, + final int priceLevel, + @Decimal final long price, + @Decimal final long size, + final long numberOfOrders) { + OrderBookOptions opt = new OrderBookOptionsBuilder() + .resetMode(ResetMode.NON_WAITING_FOR_SNAPSHOT) + .build(); + createBook(opt); + + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); + + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure book is clean + + simulateL2Insert(COINBASE, side, BEST_LEVEL, price, size, numberOfOrders); + assertExchangeBookSize(COINBASE, side, 1); // make sure insert is not ignored + + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); + + + opt = new OrderBookOptionsBuilder() + .resetMode(ResetMode.NON_WAITING_FOR_SNAPSHOT) + .build(); + createBook(opt); + + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + assertExchangeBookSizeIsEmpty(COINBASE, side); // make sure book is clean + + simulateL2Insert(COINBASE, side, BEST_LEVEL, price, size, numberOfOrders); + assertExchangeBookSize(COINBASE, side, 1); // make sure insert is not ignored + + simulateL2QuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxExchangeDepth, bbo, size, numberOfOrders); + assertExchangeBookSize(COINBASE, side, maxExchangeDepth); } + //TODO add a mode for unconsolidated date + +// @ParameterizedTest +// @MethodSource("quoteProvider") +// public void incrementalUpdate_Insert_Duplicate_L2Quote(final int depth, +// final int bbo, +// final QuoteSide side, +// final int priceLevel, +// @Decimal final long price, +// @Decimal final long size, +// final long numberOfOrders) { +// simulateL2QuoteSnapshot(PackageType.VENDOR_SNAPSHOT, COINBASE, depth, bbo, size, numberOfOrders); +// +// simulateL2Insert(COINBASE, side, priceLevel, price, size, numberOfOrders); +// assertExchangeBookSize(COINBASE, side, depth + 1); +// +// simulateL2Insert(COINBASE, side, priceLevel, price, size, numberOfOrders); +// assertExchangeBookSize(COINBASE, side, depth + 1); +// } + } diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractL3QuoteLevelTest.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractL3QuoteLevelTest.java new file mode 100644 index 0000000..6efd12a --- /dev/null +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractL3QuoteLevelTest.java @@ -0,0 +1,950 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.fwk; + +import com.epam.deltix.containers.AlphanumericUtils; +import com.epam.deltix.containers.CharSequenceUtils; +import com.epam.deltix.dfp.Decimal; +import com.epam.deltix.dfp.Decimal64Utils; +import com.epam.deltix.orderbook.core.api.MarketSide; +import com.epam.deltix.orderbook.core.api.OrderBook; +import com.epam.deltix.orderbook.core.api.OrderBookQuote; +import com.epam.deltix.orderbook.core.options.*; +import com.epam.deltix.timebase.messages.TypeConstants; +import com.epam.deltix.timebase.messages.service.FeedStatus; +import com.epam.deltix.timebase.messages.service.SecurityFeedStatusMessage; +import com.epam.deltix.timebase.messages.universal.*; +import com.epam.deltix.util.collections.generated.ObjectArrayList; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentest4j.AssertionFailedError; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; + +import static com.epam.deltix.timebase.messages.universal.PackageType.PERIODICAL_SNAPSHOT; +import static com.epam.deltix.timebase.messages.universal.PackageType.VENDOR_SNAPSHOT; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * @author Andrii_Ostapenko1 + */ +public abstract class AbstractL3QuoteLevelTest { + + public static final long COINBASE = AlphanumericUtils.toAlphanumericUInt64("COINBASE"); + public static final long BINANCE = AlphanumericUtils.toAlphanumericUInt64("BINANCE"); + + public static final String DEFAULT_SYMBOL = "BTC"; + public static final String LTC_SYMBOL = "LTC"; + public static final long DEFAULT_EXCHANGE_ID = COINBASE; + + static Stream quoteProvider() { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + + final List asks = new ArrayList<>(maxDepth); + for (int level = 0; level < maxDepth; level++) { + asks.add(arguments(maxDepth, + "id" + level, + QuoteSide.ASK, + bbo + level, + size + level, + bbo)); + } + final List bids = new ArrayList<>(maxDepth); + for (int level = 0; level < maxDepth; level++) { + bids.add(arguments(maxDepth, + "id" + maxDepth + level, + QuoteSide.BID, + bbo - level, + size + level, + bbo)); + } + return Stream.concat(asks.stream(), bids.stream()); + } + + public static Stream sideProvider() { + return Stream.of( + Arguments.of(QuoteSide.ASK), + Arguments.of(QuoteSide.BID) + ); + } + + public abstract OrderBook getBook(); + + public abstract void createBook(OrderBookOptions otherOpt); + + @Test + public void symbol_NotEmpty() { + Assertions.assertTrue(getBook().getSymbol().hasValue()); + Assertions.assertEquals(DEFAULT_SYMBOL, getBook().getSymbol().get()); + } + + // Assertion + + @ParameterizedTest + @EnumSource(value = QuoteSide.class) + public void marketSide_getTotalQuantity_zero(final QuoteSide side) { + assertTotalQuantity(side, Decimal64Utils.ZERO); + } + + /** + * Validates that the quote at the specified position in the market side matches the expected attributes. + * + * This method is crucial for ensuring data integrity and correctness within an order book in a trading system. + * It fetches a quote by its side (either ASK or BID) and position index, then compares the quote's price, size, + * and ID with the expected values. If any of the attributes do not match the expected values, this method + * should trigger an assertion failure, indicating a discrepancy in the order book or a potential issue with + * quote management. + * + *

Usage of this method is typically confined to testing scenarios, where verifying the correctness of the + * order book's state is essential.

+ * + * @param side The market side (ASK or BID) of the quote to validate. + * @param pos The position index of the quote within its side of the market. + * @param expectedPrice The expected price value of the quote for validation. + * @param expectedSize The expected size value of the quote for validation. + * @param expectedId The expected unique identifier of the quote for validation. + * + * @throws AssertionError if any of the quote's attributes (price, size, or ID) do not match the expected values. + */ + public void assertEqualPosition(final QuoteSide side, + final short pos, + final long expectedPrice, + final long expectedSize, + final CharSequence expectedId) { + final OrderBookQuote quote = getQuoteByPosition(side, pos); + assertId(quote, expectedId); + assertSize(quote, expectedSize); + assertPrice(quote, expectedPrice); + } + + public void assertBookSize(final QuoteSide side, final int expectedSize) { + final OrderBook book = getBook(); + final Iterator itr = book.getMarketSide(side).iterator(); + int actualSize = 0; + while (itr.hasNext()) { + itr.next(); + actualSize++; + } + Assertions.assertEquals(expectedSize, actualSize, "Number of " + side + "s"); + } + + public void assertExchangeBookSize(final long exchangeId, + final QuoteSide side, + final int expectedSize) { + final OrderBook book = getBook(); + final Iterator itr = book.getExchanges().getById(exchangeId).get().getMarketSide(side).iterator(); + int actualSize = 0; + while (itr.hasNext()) { + itr.next(); + actualSize++; + } + Assertions.assertEquals(expectedSize, actualSize, "Number of " + side + "s"); + } + + public void assertTotalQuantity(final QuoteSide side, + @Decimal final long expectedTotalQuantity) { + + @Decimal final long totalQuantity = getBook().getMarketSide(side).getTotalQuantity(); + Assertions.assertTrue(Decimal64Utils.isEqual(expectedTotalQuantity, totalQuantity), + "Invalid total quantity!" + + " Expected :" + Decimal64Utils.toString(expectedTotalQuantity) + + " Actual :" + Decimal64Utils.toString(totalQuantity)); + } + + public void assertId(final OrderBookQuote quote, + final CharSequence expectedId) { + Assertions.assertTrue(CharSequenceUtils.equals(expectedId, quote.getQuoteId()), + "Invalid Id!" + + " Expected :" + expectedId + + " Actual :" + quote.getQuoteId()); + } + + public void assertSize(final OrderBookQuote quote, + final long expectedSize) { + Assertions.assertTrue(Decimal64Utils.isEqual(Decimal64Utils.fromDouble(expectedSize), quote.getSize()), + "Invalid Size!" + + " Expected :" + Decimal64Utils.toString(expectedSize) + + " Actual :" + Decimal64Utils.toString(quote.getSize())); + } + + public void assertPrice(final OrderBookQuote quote, + final long expectedPrice) { + Assertions.assertTrue(Decimal64Utils.isEqual(expectedPrice, quote.getPrice()), + "Invalid Price!" + + " Expected :" + expectedPrice + + " Actual :" + quote.getPrice()); + } + + public void assertIteratorBookQuotes(final QuoteSide side, + final int expectedQuoteCounts, + final int bbo, + final int expectedSize) { + assertIteratorBookQuotes(side, expectedQuoteCounts, bbo, expectedSize, 1); + } + + // Simulator + + public void assertIteratorBookQuotes(final QuoteSide side, + final int expectedQuoteCounts, + final int bbo, + final int expectedSize, + final int numberOfExchanges) { + final Iterator iterator = getBook().getMarketSide(side).iterator(); + int iterations = 0; + int price = bbo; + while (iterator.hasNext()) { + for (int i = 0; i < numberOfExchanges; ++i) { + final OrderBookQuote quote = iterator.next(); + Assertions.assertTrue(Decimal64Utils.equals(quote.getPrice(), Decimal64Utils.fromInt(price))); + Assertions.assertTrue(Decimal64Utils.equals(Decimal64Utils.fromInt(expectedSize), quote.getSize())); + iterations++; + } + price += side == QuoteSide.ASK ? 1 : -1; + } + Assertions.assertEquals(expectedQuoteCounts, iterations); + } + + public boolean simulateInsert(final String symbol, + final long exchangeId, + final QuoteSide side, + final CharSequence quoteId, + final InsertType insertType, + final CharSequence insertBeforeQuoteId, + final double price, + final double size) { + return L3EntryNewBuilder.simulateL3EntryNew( + L3EntryNewBuilder.builder() + .setSide(side) + .setPrice(Decimal64Utils.fromDouble(price)) + .setSize(Decimal64Utils.fromDouble(size)) + .setExchangeId(exchangeId) + .setQuoteId(quoteId) + .setInsertType(insertType) + .setInsertBeforeQuoteId(insertBeforeQuoteId) // won't matter unless insertType is ADD_BEFORE + .build(), + symbol, getBook()); + } + + public void simulateCancelAllQuoteBySide(final QuoteSide side) { + final MarketSide marketSide = getBook().getMarketSide(side); + while (marketSide.getBestQuote() != null) { + final OrderBookQuote deleteQuote = getBook().getMarketSide(side).getBestQuote(); + simulateCancel(DEFAULT_EXCHANGE_ID, deleteQuote.getQuoteId()); + } + } + + public void simulateCancel(final long exchangeId, // no need to provide other fields, and they won't be checked + final CharSequence quoteId) { + simulateBookAction(exchangeId, QuoteUpdateAction.CANCEL, quoteId, QuoteSide.ASK, 1, 1); + } + + public void simulateModify(final long exchangeId, + final CharSequence quoteId, + final QuoteSide side, + @Decimal final long size, + @Decimal final long price) { + simulateBookAction(exchangeId, QuoteUpdateAction.MODIFY, quoteId, side, price, size); + } + + public void simulateReplace(final long exchangeId, + final CharSequence quoteId, + final QuoteSide side, + @Decimal final long price, + @Decimal final long size) { + simulateBookAction(exchangeId, QuoteUpdateAction.REPLACE, quoteId, side, price, size); + } + + public boolean simulateBookAction(final long exchangeId, + final QuoteUpdateAction action, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size) { + return L3EntryUpdateBuilder.simulateL3EntryUpdate( + L3EntryUpdateBuilder.builder() + .setSide(side) + .setPrice(Decimal64Utils.fromDouble(price)) + .setSize(Decimal64Utils.fromDouble(size)) + .setAction(action) + .setExchangeId(exchangeId) + .setQuoteId(quoteId) + .build(), + DEFAULT_SYMBOL, + getBook()); + } + + public PackageHeader createBookResetEntry(final PackageType packageType, final long exchangeId) { + final PackageHeader packageHeader = new PackageHeader(); + final ObjectArrayList baseEntryInfo = new ObjectArrayList<>(); + + final BookResetEntry resetEntry = new BookResetEntry(); + resetEntry.setExchangeId(exchangeId); + resetEntry.setModelType(getBook().getQuoteLevels()); + baseEntryInfo.add(resetEntry); + + packageHeader.setEntries(baseEntryInfo); + packageHeader.setSymbol(DEFAULT_SYMBOL); + packageHeader.setPackageType(packageType); + + return packageHeader; + } + + public SecurityFeedStatusMessage createSecurityFeedStatus(final long exchangeId, final FeedStatus status) { + final SecurityFeedStatusMessage message = new SecurityFeedStatusMessage(); + message.setSymbol(DEFAULT_SYMBOL); + message.setExchangeId(exchangeId); + message.setStatus(status); + return message; + } + + public boolean simulateResetEntry(final long exchangeId, final PackageType packageType) { + return getBook().update(createBookResetEntry(packageType, exchangeId)); + } + + // Book Helper + + public boolean simulateSecurityFeedStatus(final long exchangeId, final FeedStatus status) { +// return getBook().update(createSecurityFeedStatus(exchangeId, status)); + return true; + } + + public OrderBookQuote getQuoteByPosition(final QuoteSide side, + final int pos) { + final OrderBook book = getBook(); + int i = 0; + for (final OrderBookQuote quote : book.getMarketSide(side)) { + if (i++ == pos) { + return quote; + } + } + throw new AssertionFailedError(); + } + + public short getQuotePositionById(final QuoteSide side, + final CharSequence quoteId) { + final OrderBook book = getBook(); + short i = 0; + for (final OrderBookQuote quote : book.getMarketSide(side)) { + if (CharSequenceUtils.equals(quote.getQuoteId(), quoteId)) { + return i; + } + i++; + } + throw new AssertionFailedError(); + } + + public boolean simulateQuoteSnapshot(final PackageType packageType, + final long exchangeId, + final int depth, + final double bbo, + final long size) { + + final PackageHeader packageHeader = new PackageHeader(); + packageHeader.setOriginalTimestamp(System.currentTimeMillis()); + packageHeader.setTimeStampMs(System.currentTimeMillis()); + + final ObjectArrayList baseEntryInfo = new ObjectArrayList<>(); + + packageHeader.setEntries(baseEntryInfo); + packageHeader.setSymbol(DEFAULT_SYMBOL); + packageHeader.setPackageType(packageType); + + for (int j = 0; j < depth; ++j) { + final L3EntryNew entryNew = new L3EntryNew(); + entryNew.setPrice(Decimal64Utils.fromDouble(bbo + j)); + entryNew.setSize(Decimal64Utils.isNormal(size) ? size : Decimal64Utils.fromLong(size)); + entryNew.setSide(QuoteSide.ASK); + entryNew.setExchangeId(exchangeId); + entryNew.setQuoteId("id" + j); + entryNew.setInsertType(InsertType.ADD_BACK); + baseEntryInfo.add(entryNew); + } + + for (int j = 0; j < depth; ++j) { + final L3EntryNew entryNew = new L3EntryNew(); + entryNew.setPrice(Decimal64Utils.fromDouble(bbo - j)); + entryNew.setSize(Decimal64Utils.isNormal(size) ? size : Decimal64Utils.fromLong(size)); + entryNew.setSide(QuoteSide.BID); + entryNew.setExchangeId(exchangeId); + entryNew.setQuoteId("id" + depth + j); + entryNew.setInsertType(InsertType.ADD_BACK); + baseEntryInfo.add(entryNew); + } + return getBook().update(packageHeader); + } + + public PackageHeader simulateQuoteSnapshotBySide(final PackageType packageType, + final long exchangeId, + final int orderBookDepth, + final QuoteSide side, + final long bestBidAndAsk, + final long size) { + + final PackageHeader packageHeader = new PackageHeader(); + final ObjectArrayList baseEntryInfo = new ObjectArrayList<>(); + + packageHeader.setEntries(baseEntryInfo); + packageHeader.setSymbol(DEFAULT_SYMBOL); + packageHeader.setPackageType(packageType); + + for (int j = 0; j < orderBookDepth; ++j) { + final L3EntryNew entryNew = new L3EntryNew(); + entryNew.setPrice(Decimal64Utils.fromDouble(bestBidAndAsk + (side == QuoteSide.ASK ? j : -j))); + entryNew.setSize(Decimal64Utils.fromDouble(size)); + entryNew.setSide(side); + entryNew.setExchangeId(exchangeId); + entryNew.setQuoteId("id" + j); + entryNew.setInsertType(InsertType.ADD_BACK); + baseEntryInfo.add(entryNew); + } + + getBook().update(packageHeader); + return packageHeader; + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void shouldStoreQuoteTimestamp(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .shouldStoreQuoteTimestamps(true) + .build(); + createBook(opt); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + getBook().getMarketSide(side) + .forEach(q -> { + Assertions.assertTrue(q.hasOriginalTimestamp()); + Assertions.assertNotEquals(OrderBookQuote.TIMESTAMP_UNKNOWN, q.getTimestamp()); + }); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void shouldNotStoreQuoteTimestamp(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .build(); + createBook(opt); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + getBook().getMarketSide(side) + .forEach(q -> { + Assertions.assertFalse(q.hasTimestamp()); + Assertions.assertEquals(OrderBookQuote.TIMESTAMP_UNKNOWN, q.getTimestamp()); + }); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementalUpdate_Insert_Quote_invalidSymbol(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + Assertions.assertFalse(simulateInsert(LTC_SYMBOL, COINBASE, side, quoteId, InsertType.ADD_BACK, quoteId, price, size)); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + Assertions.assertFalse(simulateInsert(LTC_SYMBOL, COINBASE, side, quoteId, InsertType.ADD_BACK, quoteId, price, size)); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should not cancel quote, because of different exchange") + public void incrementalUpdate_invalidCancel_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + simulateCancel(BINANCE, quoteId); + assertBookSize(side, maxDepth); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Should cancel quote") + public void incrementalUpdate_Cancel_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + simulateCancel(COINBASE, quoteId); + assertBookSize(side, maxDepth - 1); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Trying to update the book waiting for snapshot") + public void incrementalUpdate_waitingSnapshot_Insert_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, quoteId, InsertType.ADD_BACK, quoteId, price, size); + assertBookSize(side, 0); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + @DisplayName("Trying to update the book not waiting for snapshot") + public void incrementalUpdate_Insert_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .updateMode(UpdateMode.NON_WAITING_FOR_SNAPSHOT) + .build(); + createBook(opt); + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, quoteId, InsertType.ADD_BACK, quoteId, price, size); + assertBookSize(side, 1); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementalUpdate_Modify_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateModify(COINBASE, quoteId, side, size - 1, price); + assertBookSize(side, maxDepth); + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, + mode = EnumSource.Mode.INCLUDE, + names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) + public void bbo_L3Quote(final PackageType packageType) { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + + Assertions.assertNull(getBook().getMarketSide(QuoteSide.BID).getBestQuote()); + Assertions.assertNull(getBook().getMarketSide(QuoteSide.BID).getWorstQuote()); + + Assertions.assertNull(getBook().getMarketSide(QuoteSide.ASK).getBestQuote()); + Assertions.assertNull(getBook().getMarketSide(QuoteSide.ASK).getWorstQuote()); + + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + + final int expectedWorstBid = bbo - maxDepth + 1; + final int expectedWorstAsk = bbo + maxDepth - 1; + + Assertions.assertEquals(expectedWorstBid, Decimal64Utils.toInt(getBook().getMarketSide(QuoteSide.BID).getWorstQuote().getPrice())); + Assertions.assertEquals(bbo, Decimal64Utils.toInt(getBook().getMarketSide(QuoteSide.BID).getBestQuote().getPrice())); + Assertions.assertEquals(bbo, Decimal64Utils.toInt(getBook().getMarketSide(QuoteSide.ASK).getBestQuote().getPrice())); + Assertions.assertEquals(expectedWorstAsk, Decimal64Utils.toInt(getBook().getMarketSide(QuoteSide.ASK).getWorstQuote().getPrice())); + + getBook().clear(); + + Assertions.assertNull(getBook().getMarketSide(QuoteSide.BID).getBestQuote()); + Assertions.assertNull(getBook().getMarketSide(QuoteSide.BID).getWorstQuote()); + + Assertions.assertNull(getBook().getMarketSide(QuoteSide.ASK).getBestQuote()); + Assertions.assertNull(getBook().getMarketSide(QuoteSide.ASK).getWorstQuote()); + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, + mode = EnumSource.Mode.INCLUDE, + names = {"VENDOR_SNAPSHOT"}) + public void snapshot_clear(final PackageType packageType) { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + + getBook().clear(); + assertBookSize(QuoteSide.BID, 0); + assertBookSize(QuoteSide.ASK, 0); + + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, + mode = EnumSource.Mode.INCLUDE, + names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) + public void snapshot_totalQuantity_L3Quote(final PackageType packageType) { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + + @Decimal final long expectedTotalQuantity = Decimal64Utils.fromInt(size * maxDepth); + + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + assertTotalQuantity(QuoteSide.BID, expectedTotalQuantity); + assertTotalQuantity(QuoteSide.ASK, expectedTotalQuantity); + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, + mode = EnumSource.Mode.INCLUDE, + names = {"VENDOR_SNAPSHOT", "PERIODICAL_SNAPSHOT"}) + public void incrementalUpdate_addAboveSnapshotDepth_L3Quote(final PackageType packageType) { + final int maxDepth = 10; + final int bbo = 250; + final int size = 5; + + int id = maxDepth; + for (final QuoteSide side : new QuoteSide[]{QuoteSide.ASK, QuoteSide.BID}) { + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + for (int i = 0; i < maxDepth; i++) { + simulateInsert(DEFAULT_SYMBOL, + COINBASE, + side, + "id" + id++, + InsertType.ADD_BACK, + null, + bbo + maxDepth + i, + size); + assertBookSize(side, maxDepth + i + 1); + } + } + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementUpdate_Cancel_Insert_L3Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + simulateCancel(COINBASE, quoteId); + assertBookSize(side, maxDepth - 1); + simulateInsert(DEFAULT_SYMBOL, + COINBASE, + side, + quoteId, + InsertType.ADD_BACK, + null, + bbo + (side == QuoteSide.ASK ? maxDepth : -maxDepth), + size); + assertBookSize(side, maxDepth); + } + + @ParameterizedTest + @MethodSource("sideProvider") + public void snapshot_Quote_snapshotAfterDelete(final QuoteSide side) { + final int maxDepth = 10; + final int bbo = 25; + final int size = 5; + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + simulateCancel(COINBASE, "id6"); + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + } + + @Test + public void book_isEmpty() { + assertBookSize(QuoteSide.BID, 0); + assertBookSize(QuoteSide.ASK, 0); + Assertions.assertTrue(getBook().isEmpty()); + } + + @ParameterizedTest + @EnumSource(value = PackageType.class, + mode = EnumSource.Mode.INCLUDE, + names = {"VENDOR_SNAPSHOT"} + ) + public void snapshot_byOneSide(final PackageType packageType) { + final int maxDepth = 4; + final int bbo = 25; + final int size = 5; + +// Filling two side 4:4 + simulateQuoteSnapshot(packageType, COINBASE, maxDepth, bbo, size); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, maxDepth); +// Cleaning one side 0:4 + simulateCancelAllQuoteBySide(QuoteSide.BID); + assertBookSize(QuoteSide.BID, 0); + assertBookSize(QuoteSide.ASK, maxDepth); +// Filling opposite side 4:0 + simulateQuoteSnapshotBySide(packageType, COINBASE, maxDepth, QuoteSide.BID, bbo, size); + assertBookSize(QuoteSide.BID, maxDepth); + assertBookSize(QuoteSide.ASK, 0); +// Filling two side 2:2 + simulateQuoteSnapshot(packageType, COINBASE, 2, bbo, size); + assertBookSize(QuoteSide.BID, 2); + assertBookSize(QuoteSide.ASK, 2); +// Filling two side 4:4 + simulateQuoteSnapshot(packageType, COINBASE, 4, bbo, size); + assertBookSize(QuoteSide.BID, 4); + assertBookSize(QuoteSide.ASK, 4); + } + +// @ParameterizedTest +// @MethodSource("quoteProvider") +// @Skip +// public void securityStatusMessage_Quote(final int maxDepth, +// final CharSequence quoteId, +// final QuoteSide side, +// final long price, +// final long size, +// final long bbo) { +// simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); +// +// simulateSecurityFeedStatus(COINBASE, FeedStatus.NOT_AVAILABLE); +// assertBookSize(side, 0); // make sure book is clean +// +// simulateInsert(DEFAULT_SYMBOL, COINBASE, side, quoteId, InsertType.ADD_BACK, null , price, size); +// assertBookSize(side, 0); // make sure insert is ignored (we are waiting for snapshot) +// +// simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); +// assertBookSize(side, maxDepth); +// } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_Quote_WaitingSnapshot(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .resetMode(ResetMode.WAITING_FOR_SNAPSHOT) + .build(); + createBook(opt); + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + assertBookSize(side, 0); // make sure book is clean + + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, quoteId, InsertType.ADD_BACK, null, price, size); + assertBookSize(side, 0); // make sure insert is ignored (we are waiting for snapshot) + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_Quote_PeriodicalSnapshotMode_ONLY_ONE(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .periodicalSnapshotMode(PeriodicalSnapshotMode.ONLY_ONE) + .build(); + createBook(opt); + + simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + assertBookSize(side, 0); // make sure book is clean + + simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, "id" + 2 * maxDepth, InsertType.ADD_BACK, null, price, size); + assertBookSize(side, maxDepth + 1); // make sure insert is processed + + Assertions.assertFalse(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + assertBookSize(side, maxDepth + 1); // make sure book is ignore periodical snapshot after insert + + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + Assertions.assertTrue(simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + Assertions.assertFalse(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_Quote_PeriodicalSnapshotMode_SKIP_ALL(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .periodicalSnapshotMode(PeriodicalSnapshotMode.SKIP_ALL) + .build(); + createBook(opt); + + Assertions.assertFalse(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + Assertions.assertTrue(simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + assertBookSize(side, maxDepth); + + Assertions.assertFalse(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + Assertions.assertFalse(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + assertBookSize(side, 0); // make sure book is clean + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_Quote_PeriodicalSnapshotMode_PROCESS_ALL(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .periodicalSnapshotMode(PeriodicalSnapshotMode.PROCESS_ALL) + .build(); + createBook(opt); + + Assertions.assertTrue(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + assertBookSize(side, maxDepth); + Assertions.assertTrue(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + assertBookSize(side, maxDepth); + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + + Assertions.assertTrue(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + assertBookSize(side, maxDepth); + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + Assertions.assertTrue(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + assertBookSize(side, maxDepth); + + simulateResetEntry(COINBASE, PERIODICAL_SNAPSHOT); + Assertions.assertTrue(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + assertBookSize(side, maxDepth); + Assertions.assertTrue(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, maxDepth, bbo, size)); + assertBookSize(side, maxDepth); + } + + @Test + public void resetEntry() { + simulateResetEntry(DEFAULT_EXCHANGE_ID, VENDOR_SNAPSHOT); + assertBookSize(QuoteSide.BID, 0); + assertBookSize(QuoteSide.ASK, 0); + Assertions.assertTrue(getBook().isEmpty()); + } + + @Test + public void resetEntry_Invalid() { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .resetMode(ResetMode.WAITING_FOR_SNAPSHOT) + .build(); + createBook(opt); + Assertions.assertTrue(simulateQuoteSnapshot(PERIODICAL_SNAPSHOT, COINBASE, 25, 25, 25)); + Assertions.assertFalse(simulateResetEntry(TypeConstants.EXCHANGE_NULL, VENDOR_SNAPSHOT)); + Assertions.assertFalse(getBook().isEmpty()); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void resetEntry_Quote_NoWaitingSnapshot(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .resetMode(ResetMode.NON_WAITING_FOR_SNAPSHOT) + .build(); + createBook(opt); + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + assertBookSize(side, 0); // make sure book is clean + + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, quoteId, InsertType.ADD_BACK, null, price, size); + assertBookSize(side, 1); // make sure insert is not ignored + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + + createBook(opt); + + simulateResetEntry(COINBASE, VENDOR_SNAPSHOT); + assertBookSize(side, 0); // make sure book is clean + + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, quoteId, InsertType.ADD_BACK, null, price, size); + assertBookSize(side, 1); // make sure insert is not ignored + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + assertBookSize(side, maxDepth); + } + + @ParameterizedTest + @MethodSource("quoteProvider") + public void incrementalUpdate_Insert_Duplicate_Quote(final int maxDepth, + final CharSequence quoteId, + final QuoteSide side, + final long price, + final long size, + final long bbo) { + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, "id" + 2 * maxDepth, InsertType.ADD_BACK, null, price, size); + assertBookSize(side, maxDepth + 1); + + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, "id" + 2 * maxDepth, InsertType.ADD_BACK, null, price, size); + assertBookSize(side, 0); // clears on invalidUpdate + + final ValidationOptions validationOptions = ValidationOptions.builder() + .skipInvalidQuoteInsert() + .build(); + final OrderBookOptions opt = new OrderBookOptionsBuilder() + .validationOptions(validationOptions) + .build(); + createBook(opt); + + simulateQuoteSnapshot(VENDOR_SNAPSHOT, COINBASE, maxDepth, bbo, size); + + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, "id" + 2 * maxDepth, InsertType.ADD_BACK, null, price, size); + assertBookSize(side, maxDepth + 1); + + simulateInsert(DEFAULT_SYMBOL, COINBASE, side, "id" + 2 * maxDepth, InsertType.ADD_BACK, null, price, size); + assertBookSize(side, maxDepth + 1); // skips invalidUpdate + } + +} + diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractOrderBookTest.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractOrderBookTest.java index e8e723b..2ec8aa0 100644 --- a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractOrderBookTest.java +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/AbstractOrderBookTest.java @@ -20,10 +20,13 @@ import com.epam.deltix.dfp.Decimal; import com.epam.deltix.dfp.Decimal64Utils; import com.epam.deltix.orderbook.core.api.Exchange; +import com.epam.deltix.orderbook.core.api.MarketSide; import com.epam.deltix.orderbook.core.api.OrderBook; import com.epam.deltix.orderbook.core.api.OrderBookQuote; import com.epam.deltix.orderbook.core.options.Option; import com.epam.deltix.orderbook.core.options.OrderBookOptions; +import com.epam.deltix.timebase.messages.service.FeedStatus; +import com.epam.deltix.timebase.messages.service.SecurityFeedStatusMessage; import com.epam.deltix.timebase.messages.universal.*; import com.epam.deltix.util.collections.generated.ObjectArrayList; import org.junit.jupiter.api.Assertions; @@ -73,18 +76,18 @@ public void assertionBookSideSize(final int expectedSize, final QuoteSide side, final OrderBook book) { final Iterator itr = book.getMarketSide(side).iterator(); - int i = 0; + int actualSize = 0; while (itr.hasNext()) { itr.next(); - i++; + actualSize++; } - Assertions.assertEquals(expectedSize, i); + Assertions.assertEquals(expectedSize, actualSize, "Number of " + side + "s"); } public void assertEqualLevel(final QuoteSide side, final short level, - @Decimal final long expectedPrice, - @Decimal final long expectedSize, + final long expectedPrice, + final long expectedSize, long expectedNumberOfOrders) { assertPrice(side, level, expectedPrice); assertSize(side, level, expectedSize); @@ -103,9 +106,9 @@ public void assertNotEqualLevel(final QuoteSide side, public void assertSize(final QuoteSide side, final short priceLevel, - @Decimal final long expectedSize) { + final long expectedSize) { final OrderBookQuote quote = getQuoteByLevel(side, priceLevel, getBook()); - Assertions.assertTrue(Decimal64Utils.isEqual(expectedSize, quote.getSize()), + Assertions.assertTrue(Decimal64Utils.isEqual(Decimal64Utils.fromDouble(expectedSize), quote.getSize()), "Invalid Size!" + " Expected :" + Decimal64Utils.toString(expectedSize) + " Actual :" + Decimal64Utils.toString(quote.getSize())); @@ -122,16 +125,16 @@ public void assertTotalQuantity(final QuoteSide side, } public void assertPrice(final QuoteSide side, - final short level, - @Decimal final long expectedPrice) { + final int level, + final double expectedPrice) { final OrderBookQuote quote = getQuoteByLevel(side, level, getBook()); - Assertions.assertTrue(Decimal64Utils.isEqual(expectedPrice, quote.getPrice()), + Assertions.assertTrue(Decimal64Utils.isEqual(Decimal64Utils.fromDouble(expectedPrice), quote.getPrice()), "Invalid Price!" + - " Expected :" + Decimal64Utils.toString(expectedPrice) + - " Actual :" + Decimal64Utils.toString(quote.getPrice())); + " Expected :" + expectedPrice + + " Actual :" + quote.getPrice()); } - public void assertNumberOfOrders(final QuoteSide side, final short level, final long expectedNumberOfOrders) { + public void assertNumberOfOrders(final QuoteSide side, final int level, final long expectedNumberOfOrders) { final OrderBookQuote quote = getQuoteByLevel(side, level, getBook()); Assertions.assertEquals(expectedNumberOfOrders, quote.getNumberOfOrders(), () -> "Invalid number of orders!" + @@ -182,6 +185,14 @@ public void assertBookSizeBySides(final int count) { assertionBookSideSize(count, QuoteSide.BID, getBook()); } + public void assertExchangeBookSizeIsEmpty(final long exchangeId, final QuoteSide side) { + assertExchangeBookSize(exchangeId, side, 0); + } + + public void assertExchangeBookSizeIsEmpty(final QuoteSide side) { + assertExchangeBookSize(DEFAULT_EXCHANGE_ID, side, 0); + } + public void assertExchangeBookSize(final long exchangeId, final QuoteSide side, final int expectedSize) { final Option> exchange = getBook().getExchanges().getById(exchangeId); Assertions.assertTrue(exchange.hasValue()); @@ -241,8 +252,8 @@ public void assertIteratorBookQuotes(final int exchangeCount, // Simulator public boolean simulateL1Insert(final QuoteSide side, - @Decimal final long price, - @Decimal final long size, + final long price, + final long size, final long numOfOrders) { return simulateL1Insert(DEFAULT_SYMBOL, DEFAULT_EXCHANGE_ID, side, price, size, numOfOrders); } @@ -250,14 +261,14 @@ public boolean simulateL1Insert(final QuoteSide side, public boolean simulateL1Insert(final String symbol, final long exchangeId, final QuoteSide side, - @Decimal final long price, - @Decimal final long size, + final long price, + final long size, final long numOfOrders) { return L1EntryNewBuilder.simulateL1EntryNew( L1EntryNewBuilder.builder() .setSide(side) - .setPrice(price) - .setSize(size) + .setPrice(Decimal64Utils.fromDouble(price)) + .setSize(Decimal64Utils.fromDouble(size)) .setExchangeId(exchangeId) .setNumberOfOrders(numOfOrders) .build(), @@ -283,9 +294,9 @@ public void simulateL2Insert(final QuoteSide side, public boolean simulateL2Insert(final long exchangeId, final QuoteSide side, - final short level, - @Decimal final long price, - @Decimal final long size, + final int level, + final double price, + final double size, final long numberOfOrders) { return simulateL2Insert(DEFAULT_SYMBOL, exchangeId, side, level, price, size, numberOfOrders); } @@ -293,33 +304,41 @@ public boolean simulateL2Insert(final long exchangeId, public boolean simulateL2Insert(final String symbol, final long exchangeId, final QuoteSide side, - final short level, - @Decimal final long price, - @Decimal final long size, + final int level, + final double price, + final double size, final long numberOfOrders) { return L2EntryNewBuilder.simulateL2EntryNew( L2EntryNewBuilder.builder() .setSide(side) - .setPrice(price) - .setSize(size) + .setPrice(Decimal64Utils.fromDouble(price)) + .setSize(Decimal64Utils.fromDouble(size)) .setNumberOfOrders(numberOfOrders) .setExchangeId(exchangeId) - .setLevel(level) + .setLevel((short) level) .build(), symbol, getBook()); } public void simulateL2Delete(final QuoteSide side, final short level, - @Decimal final long price, - @Decimal final long size, + final long price, + final long size, final long numberOfOrders) { simulateBookAction(DEFAULT_EXCHANGE_ID, BookUpdateAction.DELETE, side, level, price, size, numberOfOrders); } + public void simulateL2DeleteAllQuoteBySide(final QuoteSide side) { + final MarketSide marketSide = getBook().getMarketSide(side); + while (marketSide.getBestQuote() != null) { + final OrderBookQuote deleteQuote = getBook().getMarketSide(side).getBestQuote(); + simulateL2Delete(side, (short) 0, deleteQuote.getPrice(), deleteQuote.getSize(), deleteQuote.getNumberOfOrders()); + } + } + public boolean simulateL2Delete(final long exchangeId, final QuoteSide side, - final short level, + final int level, @Decimal final long price, @Decimal final long size, final long numberOfOrders) { @@ -338,40 +357,45 @@ public void simulateL2Delete(final long numberOfDeletedQuotes, } public void simulateL2Update(final QuoteSide side, - final short level, - @Decimal final long price, - @Decimal final long size, + final int level, + final long price, + final long size, final long numberOfOrders) { simulateBookAction(DEFAULT_EXCHANGE_ID, BookUpdateAction.UPDATE, side, level, price, size, numberOfOrders); } public void simulateL2Update(final long exchangeId, final QuoteSide side, - final short level, + final int level, @Decimal final long price, @Decimal final long size, final long numberOfOrders) { simulateBookAction(exchangeId, BookUpdateAction.UPDATE, side, level, price, size, numberOfOrders); } - public void simulateResetEntry(final PackageType packageType, final long exchangeId) { - getBook().update(createBookResetEntry(packageType, exchangeId)); + public boolean simulateResetEntry(final long exchangeId, final PackageType packageType) { + return getBook().update(createBookResetEntry(packageType, exchangeId)); + } + + public boolean simulateSecurityFeedStatus(final long exchangeId, final FeedStatus status) { + return getBook().update(createSecurityFeedStatus(exchangeId, status)); } + public boolean simulateBookAction(final long exchangeId, final BookUpdateAction action, final QuoteSide side, - final short level, - @Decimal final long price, - @Decimal final long size, + final int level, + final long price, + final long size, final long numberOfOrders) { return L2EntryUpdateBuilder.simulateL2EntryUpdate( L2EntryUpdateBuilder.builder() .setSide(side) - .setPrice(price) - .setSize(size) + .setPrice(Decimal64Utils.fromDouble(price)) + .setSize(Decimal64Utils.fromDouble(size)) .setNumberOfOrders(numberOfOrders) - .setLevel(level) + .setLevel((short) level) .setAction(action) .setExchangeId(exchangeId) .build(), @@ -382,7 +406,7 @@ public boolean simulateBookAction(final long exchangeId, // Book Helper public OrderBookQuote getQuoteByLevel(final QuoteSide side, - final short priceLevel, + final int priceLevel, final OrderBook book) { final Iterator itr = book.getMarketSide(side).iterator(); int i = 0; @@ -396,24 +420,46 @@ public OrderBookQuote getQuoteByLevel(final QuoteSide side, throw new AssertionFailedError(); } - public PackageHeader simulateL2QuoteSnapshot(final PackageType packageType, - final long exchangeId, - final int orderBookDepth, - final long bestBidAndAsk, - final long size, - final long numberOfOrders) { + public boolean simulateL2QuoteSnapshot(final PackageType packageType, + final long exchangeId, + final int depth, + final double bbo, + final long size, + final long numberOfOrders) { + return simulateL2QuoteSnapshot(packageType, exchangeId, depth, bbo, size, numberOfOrders, true); + } + + public boolean simulateL2QuoteSnapshot(final PackageType packageType, + final long exchangeId, + final int depth, + final double bbo, + final long size, + final long numberOfOrders, + final boolean addStatistics) { final PackageHeader packageHeader = new PackageHeader(); + packageHeader.setOriginalTimestamp(System.currentTimeMillis()); + packageHeader.setTimeStampMs(System.currentTimeMillis()); + final ObjectArrayList baseEntryInfos = new ObjectArrayList<>(); packageHeader.setEntries(baseEntryInfos); packageHeader.setSymbol(DEFAULT_SYMBOL); packageHeader.setPackageType(packageType); - for (int j = 0; j < orderBookDepth; ++j) { + // do it in the middle - kind of messy way to see if this breaks OB processor + if (addStatistics) { + final StatisticsEntry openInterest = new StatisticsEntry(); + openInterest.setType(StatisticsType.OPEN_INTEREST); + openInterest.setValue(Decimal64Utils.parse("12345.678")); + openInterest.setExchangeId(exchangeId); + baseEntryInfos.add(openInterest); + } + + for (int j = 0; j < depth; ++j) { final L2EntryNew entryNew = new L2EntryNew(); - entryNew.setPrice(Decimal64Utils.fromDouble(bestBidAndAsk + j)); - entryNew.setSize(Decimal64Utils.fromDouble(size)); + entryNew.setPrice(Decimal64Utils.fromDouble(getExpectedQuotePrice(QuoteSide.ASK, bbo, j))); + entryNew.setSize(Decimal64Utils.isNormal(size) ? size : Decimal64Utils.fromLong(size)); entryNew.setLevel((short) j); entryNew.setSide(QuoteSide.ASK); entryNew.setExchangeId(exchangeId); @@ -421,27 +467,120 @@ public PackageHeader simulateL2QuoteSnapshot(final PackageType packageType, baseEntryInfos.add(entryNew); } - for (int j = 0; j < orderBookDepth; ++j) { + // do it in the middle - kind of messy way to see if this breaks OB processor + if (addStatistics) { + final StatisticsEntry tradeVolume = new StatisticsEntry(); + tradeVolume.setType(StatisticsType.TRADE_VOLUME); + tradeVolume.setValue(Decimal64Utils.parse("12345.678")); + tradeVolume.setExchangeId(exchangeId); + baseEntryInfos.add(tradeVolume); + } + + for (int j = 0; j < depth; ++j) { final L2EntryNew entryNew = new L2EntryNew(); - entryNew.setPrice(Decimal64Utils.fromDouble(bestBidAndAsk - j)); - entryNew.setSize(Decimal64Utils.fromDouble(size)); + entryNew.setPrice(Decimal64Utils.fromDouble(getExpectedQuotePrice(QuoteSide.BID, bbo, j))); + entryNew.setSize(Decimal64Utils.isNormal(size) ? size : Decimal64Utils.fromLong(size)); entryNew.setLevel((short) j); entryNew.setSide(QuoteSide.BID); entryNew.setExchangeId(exchangeId); entryNew.setNumberOfOrders(numberOfOrders); baseEntryInfos.add(entryNew); } + + // do it at the end - kind of messy way to see if this breaks OB processor + if (addStatistics) { + final StatisticsEntry settlementPrice = new StatisticsEntry(); + settlementPrice.setType(StatisticsType.SETTLEMENT_PRICE); + settlementPrice.setValue(Decimal64Utils.parse("12345.678")); + settlementPrice.setExchangeId(exchangeId); + baseEntryInfos.add(settlementPrice); + } + + return getBook().update(packageHeader); + } + + protected double getExpectedQuotePrice(final QuoteSide side, + final double bbo, + final int level) { + if (side == QuoteSide.BID) { + return bbo - level; + } else { + return bbo + level; + } + } + + public PackageHeader simulateL2QuoteSnapshotBySide(final PackageType packageType, + final long exchangeId, + final int orderBookDepth, + final QuoteSide side, + final long bestBidAndAsk, + final long size, + final long numberOfOrders, + final boolean addStatistics) { + + final PackageHeader packageHeader = new PackageHeader(); + final ObjectArrayList baseEntryInfos = new ObjectArrayList<>(); + + packageHeader.setEntries(baseEntryInfos); + packageHeader.setSymbol(DEFAULT_SYMBOL); + packageHeader.setPackageType(packageType); + + // do it in the middle - kind of messy way to see if this breaks OB processor + if (addStatistics) { + final StatisticsEntry openInterest = new StatisticsEntry(); + openInterest.setType(StatisticsType.OPEN_INTEREST); + openInterest.setValue(Decimal64Utils.parse("12345.678")); + openInterest.setExchangeId(exchangeId); + baseEntryInfos.add(openInterest); + } + + if (side.equals(QuoteSide.ASK)) { + for (int j = 0; j < orderBookDepth; ++j) { + final L2EntryNew entryNew = new L2EntryNew(); + entryNew.setPrice(Decimal64Utils.fromDouble(bestBidAndAsk + j)); + entryNew.setSize(Decimal64Utils.fromDouble(size)); + entryNew.setLevel((short) j); + entryNew.setSide(QuoteSide.ASK); + entryNew.setExchangeId(exchangeId); + entryNew.setNumberOfOrders(numberOfOrders); + baseEntryInfos.add(entryNew); + } + } else if (side.equals(QuoteSide.BID)) { + for (int j = 0; j < orderBookDepth; ++j) { + final L2EntryNew entryNew = new L2EntryNew(); + entryNew.setPrice(Decimal64Utils.fromDouble(bestBidAndAsk - j)); + entryNew.setSize(Decimal64Utils.fromDouble(size)); + entryNew.setLevel((short) j); + entryNew.setSide(QuoteSide.BID); + entryNew.setExchangeId(exchangeId); + entryNew.setNumberOfOrders(numberOfOrders); + baseEntryInfos.add(entryNew); + } + } + + if (addStatistics) { + final StatisticsEntry tradeVolume = new StatisticsEntry(); + tradeVolume.setType(StatisticsType.TRADE_VOLUME); + tradeVolume.setValue(Decimal64Utils.parse("12345.678")); + tradeVolume.setExchangeId(exchangeId); + baseEntryInfos.add(tradeVolume); + } + getBook().update(packageHeader); return packageHeader; } - public PackageHeader simulateL1QuoteSnapshot(final PackageType packageType, - final long exchangeId, - final long bestBidAndAsk, - final long size, - final long numberOfOrders) { + public boolean simulateL1QuoteSnapshot(final PackageType packageType, + final long exchangeId, + final long bestBidAndAsk, + final long size, + final long numberOfOrders) { final PackageHeader packageHeader = new PackageHeader(); + final long now = System.currentTimeMillis(); + packageHeader.setTimeStampMs(now); + packageHeader.setOriginalTimestamp(now); + final ObjectArrayList baseEntryInfos = new ObjectArrayList<>(); packageHeader.setEntries(baseEntryInfos); @@ -465,14 +604,13 @@ public PackageHeader simulateL1QuoteSnapshot(final PackageType packageType, entryNew.setNumberOfOrders(numberOfOrders); baseEntryInfos.add(entryNew); - getBook().update(packageHeader); - return packageHeader; + return getBook().update(packageHeader); } - public PackageHeader simulateL1QuoteSnapshot(final PackageType packageType, - final long bestBidAndAsk, - final long size, - final long numberOfOrders) { + public boolean simulateL1QuoteSnapshot(final PackageType packageType, + final long bestBidAndAsk, + final long size, + final long numberOfOrders) { return this.simulateL1QuoteSnapshot(packageType, DEFAULT_EXCHANGE_ID, bestBidAndAsk, size, numberOfOrders); } @@ -493,4 +631,12 @@ public PackageHeader createBookResetEntry(final PackageType packageType, final l return packageHeader; } + public SecurityFeedStatusMessage createSecurityFeedStatus(final long exchangeId, final FeedStatus status) { + final SecurityFeedStatusMessage message = new SecurityFeedStatusMessage(); + message.setSymbol(DEFAULT_SYMBOL); + message.setExchangeId(exchangeId); + message.setStatus(status); + return message; + } + } diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L1EntryNewBuilder.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L1EntryNewBuilder.java index 38cda65..d17e087 100644 --- a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L1EntryNewBuilder.java +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L1EntryNewBuilder.java @@ -22,6 +22,7 @@ import com.epam.deltix.timebase.messages.universal.*; import com.epam.deltix.util.collections.generated.ObjectArrayList; + /** * @author Andrii_Ostapenko1 */ diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L2EntryNewBuilder.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L2EntryNewBuilder.java index e73b981..d865cd8 100644 --- a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L2EntryNewBuilder.java +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L2EntryNewBuilder.java @@ -25,6 +25,7 @@ import com.epam.deltix.timebase.messages.universal.QuoteSide; import com.epam.deltix.util.collections.generated.ObjectArrayList; + /** * @author Andrii_Ostapenko1 */ diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L2EntryUpdateBuilder.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L2EntryUpdateBuilder.java index 09e7253..9c6777c 100644 --- a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L2EntryUpdateBuilder.java +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L2EntryUpdateBuilder.java @@ -22,6 +22,7 @@ import com.epam.deltix.timebase.messages.universal.*; import com.epam.deltix.util.collections.generated.ObjectArrayList; + /** * @author Andrii_Ostapenko1 */ diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L3EntryNewBuilder.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L3EntryNewBuilder.java new file mode 100644 index 0000000..da29818 --- /dev/null +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L3EntryNewBuilder.java @@ -0,0 +1,93 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.fwk; + +import com.epam.deltix.dfp.Decimal; +import com.epam.deltix.orderbook.core.api.OrderBook; +import com.epam.deltix.orderbook.core.api.OrderBookQuote; +import com.epam.deltix.timebase.messages.universal.*; +import com.epam.deltix.util.collections.generated.ObjectArrayList; + +/** + * @author Andrii_Ostapenko1 + */ +public class L3EntryNewBuilder { + + + private final L3EntryNew entry; + + public L3EntryNewBuilder(final L3EntryNew entry) { + this.entry = entry; + } + + public static boolean simulateL3EntryNew(final L3EntryNew quote, + final String symbol, + final OrderBook book) { + final PackageHeader packageHeader = new PackageHeader(); + packageHeader.setSymbol(symbol); + packageHeader.setPackageType(PackageType.INCREMENTAL_UPDATE); + packageHeader.setEntries(new ObjectArrayList<>()); + packageHeader.getEntries().add(quote); + return book.update(packageHeader); + } + + public static L3EntryNewBuilder builder() { + return new L3EntryNewBuilder(new L3EntryNew()); + } + + + public L3EntryNewBuilder setSide(final QuoteSide side) { + entry.setSide(side); + return this; + + } + + public L3EntryNewBuilder setInsertType(final InsertType insertType) { + entry.setInsertType(insertType); + return this; + } + + // won't be used, since insertType is ADD_BACK only + public L3EntryNewBuilder setInsertBeforeQuoteId(final CharSequence quoteId) { + entry.setInsertBeforeQuoteId(quoteId); + return this; + } + + public L3EntryNewBuilder setPrice(@Decimal final long price) { + entry.setPrice(price); + return this; + } + + public L3EntryNewBuilder setSize(@Decimal final long size) { + entry.setSize(size); + return this; + } + + public L3EntryNewBuilder setQuoteId(final CharSequence quoteId) { + entry.setQuoteId(quoteId); + return this; + } + + public L3EntryNewBuilder setExchangeId(final long exchangeId) { + entry.setExchangeId(exchangeId); + return this; + } + + public L3EntryNew build() { + return entry; + } +} diff --git a/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L3EntryUpdateBuilder.java b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L3EntryUpdateBuilder.java new file mode 100644 index 0000000..69cd725 --- /dev/null +++ b/orderbook-core/src/test/java/com/epam/deltix/orderbook/core/fwk/L3EntryUpdateBuilder.java @@ -0,0 +1,87 @@ +/* + * Copyright 2021 EPAM Systems, Inc + * + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. Licensed under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.epam.deltix.orderbook.core.fwk; + +import com.epam.deltix.dfp.Decimal; +import com.epam.deltix.orderbook.core.api.OrderBook; +import com.epam.deltix.orderbook.core.api.OrderBookQuote; +import com.epam.deltix.timebase.messages.universal.*; +import com.epam.deltix.util.collections.generated.ObjectArrayList; + + +/** + * @author Andrii_Ostapenko1 + */ +public class L3EntryUpdateBuilder { + + private final L3EntryUpdate entry; + + public L3EntryUpdateBuilder(final L3EntryUpdate entry) { + this.entry = entry; + } + + public static L3EntryUpdateBuilder builder() { + return new L3EntryUpdateBuilder(new L3EntryUpdate()); + } + + public static boolean simulateL3EntryUpdate(final L3EntryUpdate quote, + final String symbol, + final OrderBook book) { + final PackageHeader packageHeader = new PackageHeader(); + packageHeader.setSymbol(symbol); + packageHeader.setPackageType(PackageType.INCREMENTAL_UPDATE); + packageHeader.setEntries(new ObjectArrayList<>()); + packageHeader.getEntries().add(quote); + return book.update(packageHeader); + } + + public L3EntryUpdateBuilder setSide(final QuoteSide side) { + entry.setSide(side); + return this; + } + + public L3EntryUpdateBuilder setPrice(@Decimal final long price) { + entry.setPrice(price); + return this; + } + + public L3EntryUpdateBuilder setSize(@Decimal final long size) { + entry.setSize(size); + return this; + } + + public L3EntryUpdateBuilder setExchangeId(final long exchangeId) { + entry.setExchangeId(exchangeId); + return this; + } + + + public L3EntryUpdateBuilder setAction(final QuoteUpdateAction action) { + entry.setAction(action); + return this; + } + + public L3EntryUpdateBuilder setQuoteId(final CharSequence quoteId) { + entry.setQuoteId(quoteId); + return this; + } + + public L3EntryUpdate build() { + return entry; + } + +} diff --git a/orderbook-it/src/main/java/com/epam/deltix/orderbook/it/OrderBookIT.java b/orderbook-it/src/main/java/com/epam/deltix/orderbook/it/OrderBookIT.java index 9399dfd..2071e87 100644 --- a/orderbook-it/src/main/java/com/epam/deltix/orderbook/it/OrderBookIT.java +++ b/orderbook-it/src/main/java/com/epam/deltix/orderbook/it/OrderBookIT.java @@ -47,10 +47,22 @@ public OrderBookIT(final String... args) { super(args); } - private static double readStream(final DXTickStream stream, final String symbol, final long startReadTime, final long endReadTime, final Consumer consumer) { - LOGGER.info().append("Start reading stream ").append(stream.getKey()).append("; Symbol: ").append(symbol).append("; Start time: ").append(startReadTime == Long.MIN_VALUE ? "min" : Instant.ofEpochMilli(startReadTime)).append("; End time: ").append(endReadTime == Long.MAX_VALUE ? "max" : Instant.ofEpochMilli(endReadTime)).commit(); - - try (TickCursor cursor = stream.select(startReadTime, new SelectionOptions(), null, new CharSequence[]{symbol})) { + private static double readStream(final DXTickStream stream, + final String symbol, + final long startReadTime, + final long endReadTime, + final Consumer consumer) { + LOGGER.info().append("Start reading stream ") + .append(stream.getKey()) + .append("; Symbol: ") + .append(symbol) + .append("; Start time: ") + .append(startReadTime == Long.MIN_VALUE ? "min" : Instant.ofEpochMilli(startReadTime)) + .append("; End time: ") + .append(endReadTime == Long.MAX_VALUE ? "max" : Instant.ofEpochMilli(endReadTime)).commit(); + + try (TickCursor cursor = + stream.select(startReadTime, new SelectionOptions(), null, new CharSequence[]{symbol})) { if (!cursor.next()) { LOGGER.info().append("Empty stream").commit(); return -1; @@ -95,11 +107,20 @@ private static double readStream(final DXTickStream stream, final String symbol, } } - private static void logTime(long startTime, long streamTime, long msgCount, long entriesCount) { + private static void logTime(final long startTime, + final long streamTime, + final long msgCount, + final long entriesCount) { final long timePass = System.currentTimeMillis() - startTime; final double timeSeconds = (double) timePass / 1000.0; final double sTimeMinutes = streamTime / (60 * 1000.0); - LOGGER.info("Read msgCount = " + msgCount + "; entriesCount = " + entriesCount + "; read time = " + (timeSeconds) + "; stream time (m) = " + sTimeMinutes + "; msg/s = " + (msgCount / timeSeconds) + "; entries/s = " + (entriesCount / timeSeconds) + "; time(m)/s = " + (sTimeMinutes / timeSeconds)); + LOGGER.info("Read msgCount = " + msgCount + + "; entriesCount = " + entriesCount + + "; read time = " + (timeSeconds) + + "; stream time (m) = " + sTimeMinutes + + "; msg/s = " + (msgCount / timeSeconds) + + "; entries/s = " + (entriesCount / timeSeconds) + + "; time(m)/s = " + (sTimeMinutes / timeSeconds)); } public static void main(final String[] args) { @@ -126,7 +147,9 @@ public void run() throws Throwable { endTime = Instant.parse(endTimeStr).toEpochMilli(); } - final DXTickDB db = timebaseUser != null ? TickDBFactory.createFromUrl(timebaseUrl, timebaseUser, timebasePassword) : TickDBFactory.createFromUrl(timebaseUrl); + final DXTickDB db = timebaseUser != null ? + TickDBFactory.createFromUrl(timebaseUrl, timebaseUser, timebasePassword) : + TickDBFactory.createFromUrl(timebaseUrl); db.open(true); try { final DXTickStream stream = db.getStream(streamKey); @@ -147,27 +170,77 @@ public void run() throws Throwable { final OrderBookOptions symbolOptions = new OrderBookOptionsBuilder().symbol(symbol).build(); - final OrderBookOptions l1SingleExchangeOptions = new OrderBookOptionsBuilder().parent(symbolOptions).orderBookType(OrderBookType.SINGLE_EXCHANGE).quoteLevels(DataModelType.LEVEL_ONE).initialDepth(500).initialExchangesPoolSize(1).updateMode(UpdateMode.WAITING_FOR_SNAPSHOT).build(); + final OrderBookOptions l1SingleExchangeOptions = new OrderBookOptionsBuilder() + .parent(symbolOptions) + .orderBookType(OrderBookType.SINGLE_EXCHANGE) + .quoteLevels(DataModelType.LEVEL_ONE) + .initialDepth(500) + .initialExchangesPoolSize(1) + .updateMode(UpdateMode.WAITING_FOR_SNAPSHOT).build(); final OrderBook singleExchangeL1Book = OrderBookFactory.create(l1SingleExchangeOptions); - resultBuilder.append(executed(symbol, startTime, endTime, stream, singleExchangeL1Book::update, singleExchangeL1Book.getDescription(), singleExchangeL1Book)); + resultBuilder.append(executed( + symbol, + startTime, + endTime, + stream, + singleExchangeL1Book::update, + singleExchangeL1Book.getDescription(), + singleExchangeL1Book) + ); // - final OrderBookOptions l2CommonOptions = new OrderBookOptionsBuilder().parent(symbolOptions).quoteLevels(DataModelType.LEVEL_TWO).initialDepth(40).initialExchangesPoolSize(1).updateMode(UpdateMode.WAITING_FOR_SNAPSHOT).build(); + final OrderBookOptions l2CommonOptions = new OrderBookOptionsBuilder() + .parent(symbolOptions) + .quoteLevels(DataModelType.LEVEL_TWO) + .initialDepth(40).initialExchangesPoolSize(1) + .updateMode(UpdateMode.WAITING_FOR_SNAPSHOT).build(); - final OrderBookOptions l2ConsolidatedOptions = new OrderBookOptionsBuilder().parent(l2CommonOptions).orderBookType(OrderBookType.CONSOLIDATED).build(); + final OrderBookOptions l2ConsolidatedOptions = new OrderBookOptionsBuilder() + .parent(l2CommonOptions) + .orderBookType(OrderBookType.CONSOLIDATED) + .build(); final OrderBook consolidatedBook = OrderBookFactory.create(l2ConsolidatedOptions); - resultBuilder.append(executed(symbol, startTime, endTime, stream, consolidatedBook::update, consolidatedBook.getDescription(), consolidatedBook)); - - final OrderBookOptions l2AggregatedOptions = new OrderBookOptionsBuilder().parent(l2CommonOptions).orderBookType(OrderBookType.AGGREGATED).build(); + resultBuilder.append(executed( + symbol, + startTime, + endTime, + stream, + consolidatedBook::update, + consolidatedBook.getDescription(), + consolidatedBook)); + + final OrderBookOptions l2AggregatedOptions = new OrderBookOptionsBuilder() + .parent(l2CommonOptions) + .orderBookType(OrderBookType.AGGREGATED) + .build(); final OrderBook aggregatedBook = OrderBookFactory.create(l2AggregatedOptions); - resultBuilder.append(executed(symbol, startTime, endTime, stream, aggregatedBook::update, aggregatedBook.getDescription(), aggregatedBook)); - - final OrderBookOptions l2SingledExchangeOptions = new OrderBookOptionsBuilder().parent(l2CommonOptions).orderBookType(OrderBookType.SINGLE_EXCHANGE).build(); + resultBuilder.append(executed( + symbol, + startTime, + endTime, + stream, + aggregatedBook::update, + aggregatedBook.getDescription(), + aggregatedBook) + ); + + final OrderBookOptions l2SingledExchangeOptions = new OrderBookOptionsBuilder() + .parent(l2CommonOptions) + .orderBookType(OrderBookType.SINGLE_EXCHANGE) + .build(); final OrderBook singleExchangeBook = OrderBookFactory.create(l2SingledExchangeOptions); - resultBuilder.append(executed(symbol, startTime, endTime, stream, singleExchangeBook::update, singleExchangeBook.getDescription(), singleExchangeBook)); + resultBuilder.append(executed( + symbol, + startTime, + endTime, + stream, + singleExchangeBook::update, + singleExchangeBook.getDescription(), + singleExchangeBook) + ); LOGGER.info("\n %s").with(resultBuilder); @@ -178,13 +251,22 @@ public void run() throws Throwable { } } - private String executed(final String symbol, final long startTime, final long endTime, final DXTickStream stream, final Consumer consumer, final String className, final Object book) { + private String executed(final String symbol, + final long startTime, + final long endTime, + final DXTickStream stream, + final Consumer consumer, + final String className, + final Object book) { double sum = 0; final int iteration = 20; for (int i = 0; i < iteration; i++) { sum += readStream(stream, symbol, startTime, endTime, consumer); } - return String.format("Book Name = %s, Read AVG msg/s = %d , Size bite(s) = %d, FootPrint = %s \n", className, (int) sum / iteration, GraphLayout.parseInstance(book).totalSize(), GraphLayout.parseInstance(book).toFootprint()); + return String.format("Book Name = %s, Read AVG msg/s = %d , Size bite(s) = %d, FootPrint = %s \n", + className, (int) sum / iteration, + GraphLayout.parseInstance(book).totalSize(), + GraphLayout.parseInstance(book).toFootprint()); } } diff --git a/orderbook-sample/src/main/java/com/epam/deltix/orderbook/sample/OrderBook_03_ConsolidatedOrderBook.java b/orderbook-sample/src/main/java/com/epam/deltix/orderbook/sample/OrderBook_03_ConsolidatedOrderBook.java index 35aaf5e..7ea86a5 100644 --- a/orderbook-sample/src/main/java/com/epam/deltix/orderbook/sample/OrderBook_03_ConsolidatedOrderBook.java +++ b/orderbook-sample/src/main/java/com/epam/deltix/orderbook/sample/OrderBook_03_ConsolidatedOrderBook.java @@ -61,7 +61,10 @@ public static void main(final String[] args) { final OrderBook orderBook = OrderBookFactory.create(opt); - System.out.println("Hello! I'm " + orderBook.getDescription() + " for stock symbol: " + orderBook.getSymbol().get() + "!"); + System.out.println("Hello! I'm " + + orderBook.getDescription() + + " for stock symbol: " + + orderBook.getSymbol().get() + "!"); // Step 2: feed it with updates for (final long exchangeId : exchangeIds) {