├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── Modeling-high-frequency-limit-order-book-dynamics-with-support-vector-machines.pdf ├── features.png └── messagebook.png ├── build.sbt ├── project ├── .gitignore ├── bintray.sbt └── build.properties └── src ├── main └── scala │ └── com │ └── scalafi │ └── openbook │ ├── ByteArrayIterator.scala │ ├── MsgType.scala │ ├── OpenBookMsg.scala │ ├── QuoteCondition.scala │ ├── ReasonCode.scala │ ├── Side.scala │ ├── TradingStatus.scala │ └── orderbook │ └── OrderBook.scala └── test ├── resources └── openbookultraAA_N20130403_1_of_1 └── scala └── com └── scalafi └── openbook ├── OpenBookParserSpec.scala └── orderbook ├── OrderBookSpec.scala └── package.scala /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings/ 2 | /target/ 3 | /.cache 4 | /.classpath 5 | /.history 6 | /.project 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.11.2 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Eugene Zhulenev, and respective contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TAQ NYSE OpenBook Ultra Parser 2 | 3 | [![Build Status](https://travis-ci.org/ezhulenev/scala-openbook.svg?branch=master)](https://travis-ci.org/ezhulenev/scala-openbook) 4 | 5 | Scala parser for NYSE historical data product: http://www.nyxdata.com/Data-Products/NYSE-OpenBook-History 6 | 7 | ## Where to get it 8 | 9 | To get the latest version of the library, add the following to your SBT build: 10 | 11 | ``` scala 12 | resolvers += "ezhulenev Bintray Repo" at "https://dl.bintray.com/ezhulenev/releases" 13 | ``` 14 | 15 | And use following library dependencies: 16 | 17 | ``` 18 | libraryDependencies += "com.scalafi" %% "scala-openbook" % "0.0.10" 19 | ``` 20 | -------------------------------------------------------------------------------- /assets/Modeling-high-frequency-limit-order-book-dynamics-with-support-vector-machines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhulenev/scala-openbook/45d36b1da7b539ddefecbb7727b10b20e3ee7d97/assets/Modeling-high-frequency-limit-order-book-dynamics-with-support-vector-machines.pdf -------------------------------------------------------------------------------- /assets/features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhulenev/scala-openbook/45d36b1da7b539ddefecbb7727b10b20e3ee7d97/assets/features.png -------------------------------------------------------------------------------- /assets/messagebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhulenev/scala-openbook/45d36b1da7b539ddefecbb7727b10b20e3ee7d97/assets/messagebook.png -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import bintray.Keys._ 2 | 3 | name := "Scala OpenBook" 4 | 5 | version := "0.0.10" 6 | 7 | organization := "com.scalafi" 8 | 9 | licenses in ThisBuild += ("MIT", url("http://opensource.org/licenses/MIT")) 10 | 11 | scalaVersion := "2.11.4" 12 | 13 | crossScalaVersions in ThisBuild := Seq("2.10.4", "2.11.4") 14 | 15 | scalacOptions += "-deprecation" 16 | 17 | scalacOptions += "-feature" 18 | 19 | 20 | // Resolvers 21 | 22 | resolvers ++= Seq( 23 | Resolver.sonatypeRepo("releases"), 24 | Resolver.sonatypeRepo("snapshots") 25 | ) 26 | 27 | // Test Dependencies 28 | 29 | libraryDependencies ++= Seq( 30 | "org.scalatest" %% "scalatest" % "2.2.0" % "test" 31 | ) 32 | 33 | // Configure publishing to bintray 34 | 35 | bintrayPublishSettings 36 | 37 | repository in bintray := "releases" 38 | 39 | bintrayOrganization in bintray := None 40 | -------------------------------------------------------------------------------- /project/.gitignore: -------------------------------------------------------------------------------- 1 | /project/ 2 | /target/ 3 | -------------------------------------------------------------------------------- /project/bintray.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Resolver.url( 2 | "bintray-sbt-plugin-releases", 3 | url("http://dl.bintray.com/content/sbt/sbt-plugin-releases"))( 4 | Resolver.ivyStylePatterns) 5 | 6 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.1.2") -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.5 -------------------------------------------------------------------------------- /src/main/scala/com/scalafi/openbook/ByteArrayIterator.scala: -------------------------------------------------------------------------------- 1 | package com.scalafi.openbook 2 | 3 | import java.io.InputStream 4 | 5 | /** 6 | * Wraps an input stream into an `Iterator` interface. Note that the underlying Array[Byte] 7 | * is NOT re-allocated. So the iterator itself assumes that the message is consumed / interpreted 8 | * before the next call to `next`. 9 | */ 10 | final class ByteArrayIterator(is : InputStream, len : Int) extends Iterator[Array[Byte]] { 11 | 12 | assume(len > 0, s"Message length must be positive. Found: $len") 13 | 14 | private val buf = new Array[Byte](len) 15 | private var done, full = false : Boolean 16 | 17 | def next(): Array[Byte] = { 18 | if (full) { 19 | // next message already buffered 20 | full = false 21 | buf 22 | 23 | } else { 24 | // see if we can buffer next message, if so, return that one 25 | if (hasNext) next() 26 | else throw new NoSuchElementException("next on empty iterator") 27 | } 28 | } 29 | 30 | def hasNext: Boolean = { 31 | if (done) false 32 | else if (full) true 33 | else { 34 | val nBytesRead = is.read(buf) 35 | 36 | if (nBytesRead == len) { 37 | // cool, read a whole message 38 | full = true 39 | full 40 | 41 | } else if (nBytesRead == -1) { 42 | // end of buffer has been reached 43 | is.close() 44 | done = true 45 | false 46 | 47 | } else { 48 | throw new NoSuchElementException(s"Expected message length of $len, but could read only $nBytesRead.") 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/scala/com/scalafi/openbook/MsgType.scala: -------------------------------------------------------------------------------- 1 | package com.scalafi.openbook 2 | 3 | sealed trait MsgType 4 | 5 | object MsgType { 6 | 7 | case object FullUpdate extends MsgType 8 | 9 | case object DeltaUpdate extends MsgType 10 | 11 | def apply(code: Short): MsgType = code match { 12 | case 230 => FullUpdate 13 | case 231 => DeltaUpdate 14 | case _ => sys.error(s"Unknown message type code: '$code'") 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/scala/com/scalafi/openbook/OpenBookMsg.scala: -------------------------------------------------------------------------------- 1 | package com.scalafi.openbook 2 | 3 | import java.io.InputStream 4 | import java.nio.ByteBuffer 5 | import java.io.FileInputStream 6 | import java.io.File 7 | import java.io.BufferedInputStream 8 | 9 | 10 | private[openbook] trait Parser { 11 | 12 | protected sealed trait Parser[T] { parser => 13 | 14 | def parseInternal(byteBuffer: ByteBuffer): T 15 | 16 | def parse(bytes: Array[Byte], pos: (Int, Int)): T = { 17 | val (from, to) = pos 18 | parseInternal(ByteBuffer.wrap(bytes.slice(from - 1, to))) 19 | } 20 | 21 | def map[O](f: T => O): Parser[O] = new Parser[O] { 22 | def parseInternal(byteBuffer: ByteBuffer): O = 23 | f(parser.parseInternal(byteBuffer)) 24 | } 25 | } 26 | 27 | protected implicit object IntParser extends Parser[Int] { 28 | def parseInternal(byteBuffer: ByteBuffer) = 29 | byteBuffer.getInt 30 | } 31 | 32 | protected implicit object ShortParser extends Parser[Short] { 33 | def parseInternal(byteBuffer: ByteBuffer) = 34 | byteBuffer.getShort 35 | } 36 | 37 | protected implicit object StringParser extends Parser[String] { 38 | def parseInternal(byteBuffer: ByteBuffer) = 39 | String.valueOf(byteBuffer.array().map(_.toChar)).trim 40 | } 41 | 42 | protected implicit object ByteParser extends Parser[Byte] { 43 | def parseInternal(byteBuffer: ByteBuffer) = 44 | byteBuffer.get() 45 | } 46 | 47 | protected implicit val MsgTypeParser: Parser[MsgType] = 48 | implicitly[Parser[Short]].map(MsgType.apply) 49 | 50 | protected implicit val QuoteConditionParser: Parser[QuoteCondition] = 51 | implicitly[Parser[Byte]].map(b => QuoteCondition(b.toChar)) 52 | 53 | protected implicit val TradingStatusParser: Parser[TradingStatus] = 54 | implicitly[Parser[Byte]].map(b => TradingStatus(b.toChar)) 55 | 56 | protected implicit val SideParser: Parser[Side] = 57 | implicitly[Parser[Byte]].map(b => Side(b.toChar)) 58 | 59 | protected implicit val ReasonCodeParser: Parser[ReasonCode] = 60 | implicitly[Parser[Byte]].map(b => ReasonCode(b.toChar)) 61 | 62 | protected def parse[T](pos: (Int, Int))(implicit bytes: Array[Byte], parser: Parser[T]) = 63 | parser.parse(bytes, pos) 64 | } 65 | 66 | object OpenBookMsg extends Parser { 67 | 68 | /** 69 | * Number of bytes per message: 70 | */ 71 | val msgLength = 69 72 | 73 | private object Layout { 74 | val MsgSeqNum = 1 -> 4 75 | val MsgType = 5 -> 6 76 | val SendTime = 7 -> 10 77 | val Symbol = 11 -> 21 78 | val MsgSize = 22 -> 23 79 | val SecurityIndex = 24 -> 25 80 | val SourceTime = 26 -> 29 81 | val SourceTimeMicroSecs = 30 -> 31 82 | val QuoteCondition = 32 -> 32 83 | val TradingStatus = 33 -> 33 84 | val SourceSeqNum = 34 -> 37 85 | val SourceSessionId = 38 -> 38 86 | val PriceScaleCode = 39 -> 39 87 | val PriceNumerator = 40 -> 43 88 | val Volume = 44 -> 47 89 | val ChgQty = 48 -> 51 90 | val NumOrders = 52 -> 53 91 | val Side = 54 -> 54 92 | val Filler1 = 55 -> 55 93 | val ReasonCode = 56 -> 56 94 | val Filler2 = 57 -> 57 95 | val LinkID1 = 58 -> 61 96 | val LinkID2 = 62 -> 65 97 | val LinkID3 = 66 -> 69 98 | } 99 | 100 | def apply(bytes: Array[Byte]): OpenBookMsg = { 101 | assume(bytes.length == msgLength, s"Unexpected message length: ${bytes.length}") 102 | 103 | implicit val b = bytes 104 | 105 | OpenBookMsg( 106 | parse[Int](Layout.MsgSeqNum), 107 | parse[MsgType](Layout.MsgType), 108 | parse[Int](Layout.SendTime), 109 | parse[String](Layout.Symbol), 110 | parse[Short](Layout.MsgSize), 111 | parse[Short](Layout.SecurityIndex), 112 | parse[Int](Layout.SourceTime), 113 | parse[Short](Layout.SourceTimeMicroSecs), 114 | parse[QuoteCondition](Layout.QuoteCondition), 115 | parse[TradingStatus](Layout.TradingStatus), 116 | parse[Int](Layout.SourceSeqNum), 117 | parse[Byte](Layout.SourceSessionId), 118 | parse[Byte](Layout.PriceScaleCode), 119 | parse[Int](Layout.PriceNumerator), 120 | parse[Int](Layout.Volume), 121 | parse[Int](Layout.ChgQty), 122 | parse[Short](Layout.NumOrders), 123 | parse[Side](Layout.Side), 124 | parse[ReasonCode](Layout.ReasonCode), 125 | parse[Int](Layout.LinkID1), 126 | parse[Int](Layout.LinkID2), 127 | parse[Int](Layout.LinkID3) 128 | ) 129 | } 130 | 131 | def iterate(filename: File) : Iterator[OpenBookMsg] = 132 | iterate(new BufferedInputStream(new FileInputStream(filename))) 133 | 134 | def iterate(is: InputStream) : Iterator[OpenBookMsg] = 135 | // the underlying iterator is not thread-safe, however, the returned iterator is: 136 | new ByteArrayIterator(is, msgLength).map(OpenBookMsg.apply) 137 | } 138 | 139 | 140 | case class OpenBookMsg(msgSeqNum: Int, 141 | msgType: MsgType, 142 | sendTime: Int, 143 | symbol: String, 144 | msgSize: Short, 145 | securityIndex: Short, 146 | sourceTime: Int, 147 | sourceTimeMicroSecs: Short, 148 | quoteCondition: QuoteCondition, 149 | tradingStatus: TradingStatus, 150 | sourceSeqNum: Int, 151 | sourceSessionId: Byte, 152 | priceScaleCode: Byte, 153 | priceNumerator: Int, 154 | volume: Int, 155 | chgQty: Int, 156 | numOrders: Short, 157 | side: Side, 158 | reasonCode: ReasonCode, 159 | linkID1: Int, 160 | linkID2: Int, 161 | linkID3: Int) -------------------------------------------------------------------------------- /src/main/scala/com/scalafi/openbook/QuoteCondition.scala: -------------------------------------------------------------------------------- 1 | package com.scalafi.openbook 2 | 3 | sealed trait QuoteCondition 4 | 5 | object QuoteCondition { 6 | case object Normal extends QuoteCondition 7 | case object SlowBid extends QuoteCondition 8 | case object SlowAsk extends QuoteCondition 9 | case object SlowBidAsk extends QuoteCondition 10 | case object SetSlow extends QuoteCondition 11 | 12 | def apply(c: Char) = c match { 13 | case ' ' => Normal 14 | case 'E' => SlowBid 15 | case 'F' => SlowAsk 16 | case 'U' => SlowBidAsk 17 | case 'W' => SetSlow 18 | case _ => sys.error(s"Unknown quote condition code: '$c'") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/com/scalafi/openbook/ReasonCode.scala: -------------------------------------------------------------------------------- 1 | package com.scalafi.openbook 2 | 3 | sealed trait ReasonCode 4 | 5 | object ReasonCode { 6 | case object Order extends ReasonCode 7 | case object Cancel extends ReasonCode 8 | case object Execution extends ReasonCode 9 | case object MultipleEvents extends ReasonCode 10 | case object NA extends ReasonCode 11 | 12 | def apply(c: Char) = c match { 13 | case 'O' => Order 14 | case 'C' => Cancel 15 | case 'E' => Execution 16 | case 'X' => MultipleEvents 17 | case _ if c.toByte == 0 => NA 18 | case _ => sys.error(s"Unknown reason code: '$c'") 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/scala/com/scalafi/openbook/Side.scala: -------------------------------------------------------------------------------- 1 | package com.scalafi.openbook 2 | 3 | sealed trait Side 4 | 5 | object Side { 6 | case object Buy extends Side 7 | case object Sell extends Side 8 | case object NA extends Side 9 | 10 | def apply(c: Char) = c match { 11 | case 'B' => Buy 12 | case 'S' => Sell 13 | case _ if c.toByte == 0 => NA 14 | case _ => sys.error(s"Unknown Buy/Sell side: '$c'") 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/scala/com/scalafi/openbook/TradingStatus.scala: -------------------------------------------------------------------------------- 1 | package com.scalafi.openbook 2 | 3 | sealed trait TradingStatus 4 | 5 | object TradingStatus { 6 | case object PreOpening extends TradingStatus 7 | case object Opened extends TradingStatus 8 | case object Closed extends TradingStatus 9 | case object Halted extends TradingStatus 10 | 11 | def apply(c: Char) = c match { 12 | case 'P' => PreOpening 13 | case 'O' => Opened 14 | case 'C' => Closed 15 | case 'H' => Halted 16 | case _ => sys.error(s"Unknown trading status: '$c'") 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/scala/com/scalafi/openbook/orderbook/OrderBook.scala: -------------------------------------------------------------------------------- 1 | package com.scalafi.openbook.orderbook 2 | 3 | import scala.collection.immutable.TreeMap 4 | import com.scalafi.openbook.{Side, OpenBookMsg} 5 | 6 | 7 | object OrderBook { 8 | 9 | def empty(symbol: String): OrderBook = new OrderBook(symbol) 10 | 11 | /** 12 | * Time series of OrderBook for a stream of order messages for a single symbol 13 | */ 14 | def fromOrders(symbol: String, orders: Iterator[OpenBookMsg]): Iterator[OrderBook] = { 15 | orders. 16 | filter(_.symbol == symbol). // filter by symbol 17 | scanLeft(OrderBook.empty(symbol))((ob, o) => ob.update(o)). // update orderbook 18 | drop(1) // drop empty order book used for initialization 19 | } 20 | 21 | /** 22 | * Time series of OrderBooks for a stream of order messages for a multiple symbols. 23 | * The iterator will always return the order book corresponding to the most recent message. 24 | */ 25 | def fromOrders(orders: Iterator[OpenBookMsg]): Iterator[OrderBook] = { 26 | val orderBooks = collection.mutable.Map.empty[String, OrderBook] 27 | orders map { order => 28 | // Get OrderBook for current symbol 29 | val orderBook = orderBooks.getOrElseUpdate(order.symbol, OrderBook.empty(order.symbol)) 30 | 31 | // Update OrderBook state with new order 32 | val updated = orderBook.update(order) 33 | orderBooks.update(order.symbol, updated) 34 | 35 | // Return updated state 36 | updated 37 | } 38 | } 39 | } 40 | 41 | case class OrderBook(symbol: String, 42 | buy: TreeMap[Int, Int] = TreeMap.empty, 43 | sell: TreeMap[Int, Int] = TreeMap.empty, 44 | lastMsg : OpenBookMsg = null 45 | ) { 46 | 47 | def update(order: OpenBookMsg): OrderBook = { 48 | assume(order.symbol == symbol, s"Unexpected order symbol: ${order.symbol}. In Order Book for: $symbol") 49 | 50 | order match { 51 | case _ if order.side == Side.Buy & order.volume > 0 => 52 | copy(buy = buy + (order.priceNumerator -> order.volume), lastMsg = order) 53 | 54 | case _ if order.side == Side.Buy & order.volume == 0 => 55 | copy(buy = buy - order.priceNumerator, lastMsg = order) 56 | 57 | case _ if order.side == Side.Sell & order.volume > 0 => 58 | copy(sell = sell + (order.priceNumerator -> order.volume), lastMsg = order) 59 | 60 | case _ if order.side == Side.Sell & order.volume == 0 => 61 | copy(sell = sell - order.priceNumerator, lastMsg = order) 62 | 63 | case _ if order.side == Side.NA => copy(lastMsg = order) 64 | } 65 | } 66 | 67 | def printOrderBook(depth: Int): String = { 68 | 69 | val bid = buy.keySet.drop(buy.size - depth).map(price => s"$price : ${buy(price)}"); 70 | val ask = sell.keySet.take(depth).map(price => s"$price : ${sell(price)}"); 71 | 72 | s"""|Bid 73 | |${bid.mkString(System.lineSeparator())} 74 | |- - - - - - - - - - 75 | |Ask 76 | |${ask.mkString(System.lineSeparator())} 77 | |""".stripMargin.trim 78 | } 79 | } -------------------------------------------------------------------------------- /src/test/resources/openbookultraAA_N20130403_1_of_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhulenev/scala-openbook/45d36b1da7b539ddefecbb7727b10b20e3ee7d97/src/test/resources/openbookultraAA_N20130403_1_of_1 -------------------------------------------------------------------------------- /src/test/scala/com/scalafi/openbook/OpenBookParserSpec.scala: -------------------------------------------------------------------------------- 1 | package com.scalafi.openbook 2 | 3 | import org.scalatest.FlatSpec 4 | 5 | class OpenBookParserSpec extends FlatSpec { 6 | 7 | implicit val codec = io.Codec.ISO8859 8 | 9 | "OpenBook parser" should "parse all OpenBook Ultra messages" in { 10 | 11 | val is = this.getClass.getResourceAsStream("/openbookultraAA_N20130403_1_of_1") 12 | 13 | val messages = io.Source.fromInputStream(is). 14 | map(_.toByte).grouped(69).map(_.toArray) 15 | 16 | val parsed = messages.map(OpenBookMsg.apply) 17 | assert(parsed.size == 1000) 18 | } 19 | 20 | it should "stream it from InputStream" in { 21 | val is = this.getClass.getResourceAsStream("/openbookultraAA_N20130403_1_of_1") 22 | 23 | val orders = OpenBookMsg.iterate(is) 24 | assert(orders.size == 1000) 25 | } 26 | 27 | it should "build iterator from InputStream" in { 28 | val is = this.getClass.getResourceAsStream("/openbookultraAA_N20130403_1_of_1") 29 | 30 | val orders = OpenBookMsg.iterate(is) 31 | val parsed = orders.toVector 32 | assert(parsed.size == 1000) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/scala/com/scalafi/openbook/orderbook/OrderBookSpec.scala: -------------------------------------------------------------------------------- 1 | package com.scalafi.openbook.orderbook 2 | 3 | import org.scalatest.{GivenWhenThen, FlatSpec} 4 | import com.scalafi.openbook._ 5 | 6 | class OrderBookSpec extends FlatSpec with GivenWhenThen { 7 | 8 | "OrderBook" should "build correct order book from order flow" in { 9 | 10 | Given("three orders") 11 | val order1 = orderMsg(0, 0, 10000, 10, Side.Buy) 12 | val order2 = orderMsg(100, 0, 10000, 15, Side.Buy) 13 | val order3 = orderMsg(200, 0, 11000, 20, Side.Sell) 14 | 15 | Then("correct order books should be constructed") 16 | val orderBook = OrderBook(SYMBOL_APL).update(order1).update(order2).update(order3) 17 | 18 | assert(orderBook.buy.size == 1) 19 | assert(orderBook.sell.size == 1) 20 | 21 | Given("next order") 22 | val order4 = orderMsg(1001, 0, 12000, 20, Side.Sell) 23 | 24 | Then("first order should be evicted by time") 25 | val orderBookUpd = orderBook.update(order4) 26 | 27 | assert(orderBookUpd.buy.size == 1) 28 | assert(orderBookUpd.sell.size == 2) 29 | } 30 | 31 | it should "build valid stream of order books for a single symbol " in { 32 | 33 | Given("three orders") 34 | val order1 = orderMsg(0, 0, 10000, 10, Side.Buy) 35 | val order2 = orderMsg(100, 0, 10500, 15, Side.Buy) 36 | val order3 = orderMsg(200, 0, 11000, 20, Side.Sell) 37 | 38 | val orders: Iterator[OpenBookMsg] = Seq(order1, order2, order3).iterator 39 | 40 | Then("stream of three order books should be created") 41 | val orderBooks = OrderBook.fromOrders(SYMBOL_APL, orders) 42 | 43 | val orderBook1 = orderBooks.next() 44 | assert(orderBook1.buy.size == 1) 45 | assert(orderBook1.buy.get(order1.priceNumerator).get == order1.volume) 46 | 47 | val orderBook2 = orderBooks.next() 48 | assert(orderBook2.buy.size == 2) 49 | assert(orderBook2.buy.get(order2.priceNumerator).get == order2.volume) 50 | 51 | val orderBook3 = orderBooks.next() 52 | assert(orderBook3.buy.size == 2) 53 | assert(orderBook3.sell.size == 1) 54 | assert(orderBook3.sell.get(order3.priceNumerator).get == order3.volume) 55 | 56 | assert(!orderBooks.hasNext) 57 | 58 | } 59 | 60 | it should "support a stream of messages corresponding to different symbols " in { 61 | 62 | Given("Open Book order log") 63 | def is = this.getClass.getResourceAsStream("/openbookultraAA_N20130403_1_of_1") 64 | 65 | Then("the number of symbols for in the message stream should match the number of symbols in the order book stream") 66 | val symbolsInMessages = OpenBookMsg.iterate(is).map(_.symbol).toSet 67 | val symbolsInOrderBooks = OrderBook.fromOrders(OpenBookMsg.iterate(is)).map(_.lastMsg.symbol).toSet 68 | 69 | assert(symbolsInMessages.intersect(symbolsInOrderBooks).size == symbolsInMessages.size) 70 | } 71 | } -------------------------------------------------------------------------------- /src/test/scala/com/scalafi/openbook/orderbook/package.scala: -------------------------------------------------------------------------------- 1 | package com.scalafi.openbook 2 | 3 | package object orderbook { 4 | 5 | val SYMBOL_APL = "APL" : String 6 | 7 | def orderMsg(sourceTime: Int, sourceTimeMicroSecs: Short, price: Int, volume: Int, side: Side) = 8 | OpenBookMsg( 9 | msgSeqNum = 0, 10 | msgType = MsgType.DeltaUpdate, 11 | sendTime = 0, 12 | symbol = SYMBOL_APL, 13 | msgSize = 46, 14 | securityIndex = 0, 15 | sourceTime = sourceTime + 300000, 16 | sourceTimeMicroSecs = sourceTimeMicroSecs, 17 | quoteCondition = QuoteCondition.Normal, 18 | tradingStatus = TradingStatus.Opened, 19 | sourceSeqNum = 0, 20 | sourceSessionId = 0, 21 | priceScaleCode = 4, 22 | priceNumerator = price, 23 | volume = volume, 24 | chgQty = 0, 25 | numOrders = 0, 26 | side = side, 27 | reasonCode = ReasonCode.Order, 28 | linkID1 = 0, 29 | linkID2 = 0, 30 | linkID3 = 0) 31 | 32 | } 33 | --------------------------------------------------------------------------------