diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..d6da3d1 --- /dev/null +++ b/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.ignore b/.gitignore similarity index 100% rename from .ignore rename to .gitignore diff --git a/.project b/.project new file mode 100644 index 0000000..0bf8867 --- /dev/null +++ b/.project @@ -0,0 +1,18 @@ + + + SimpleFinancialExchange + + + + + + org.scala-ide.sdt.core.scalabuilder + + + + + + org.scala-ide.sdt.core.scalanature + org.eclipse.jdt.core.javanature + + diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..997bd4b --- /dev/null +++ b/build.sbt @@ -0,0 +1,9 @@ +name := "SimpleFinancialExchange" + +version := "1.0" + +scalaVersion := "2.11.4" + +libraryDependencies += "org.scalatest" % "scalatest_2.11" % "2.2.1" % "test" + +libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.3.8" \ No newline at end of file diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..858f009 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version = 0.13.5 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..14a6ca1 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +logLevel := Level.Warn \ No newline at end of file diff --git a/src/main/scala/OrderBook.scala b/src/main/scala/OrderBook.scala new file mode 100644 index 0000000..3ac3952 --- /dev/null +++ b/src/main/scala/OrderBook.scala @@ -0,0 +1,247 @@ + +import java.util.{Comparator, PriorityQueue} + +/** * + * + * @author Shahbaz Chaudhary (shahbazc gmail com) + * + */ + + +abstract class OrderBookRequest +case class NewOrder(timestamp: Long, tradeID: String, symbol: String, qty: Long, isBuy: Boolean, price: Option[Double]) extends OrderBookRequest +case class Cancel(timestamp: Long, order: NewOrder) extends OrderBookRequest +case class Amend(timestamp: Long, order:NewOrder, newPrice:Option[Double], newQty:Option[Long]) extends OrderBookRequest + +abstract class OrderBookResponse +case class Filled(timestamp: Long, price: Double, qty: Long, order: Array[NewOrder]) extends OrderBookResponse +case class Acknowledged(timestamp: Long, request: OrderBookRequest) extends OrderBookResponse +case class Rejected(timestamp: Long, error: String, request: OrderBookRequest) extends OrderBookResponse +case class Canceled(timestamp: Long, reason: String, order: NewOrder) extends OrderBookResponse + +abstract class MarketDataEvent +case class LastSalePrice(timestamp: Long, symbol: String, price: Double, qty: Long, volume: Long) extends MarketDataEvent +case class BBOChange(timestamp: Long, symbol: String, bidPrice:Option[Double], bidQty:Option[Long], offerPrice:Option[Double], offerQty:Option[Long]) extends MarketDataEvent + + +class OrderBook(symbol: String) { + case class Order(timestamp: Long, tradeID: String, symbol: String, var qty: Long, isBuy: Boolean, var price: Option[Double], newOrderEvent:NewOrder) + + val bidOrdering = Ordering.by { order: Order => (order.timestamp, order.price.get)} + val offerOrdering = bidOrdering.reverse + + //Needed for java.util.PriorityQueue + val bidComparator = new Comparator[Order]{ + def compare(o1:Order, o2:Order):Int = bidOrdering.compare(o1,o2) + } + val offerComparator = new Comparator[Order]{ + def compare(o1:Order, o2:Order):Int = offerOrdering.compare(o1,o2) + } + + //val bidsQ = new mutable.PriorityQueue[NewOrder]()(bidOrdering) + //val offersQ = new mutable.PriorityQueue[NewOrder]()(offerOrdering) + + //scala PQ doesn't let me remove items, so must revert to Java's PQ + val bidsQ = new PriorityQueue[Order](5,bidComparator) + val offersQ = new PriorityQueue[Order](5,offerComparator) + + var bestBid: Option[Order] = None + var bestOffer: Option[Order] = None + var volume: Long = 0 + + var transactionObserver: (OrderBookResponse) => Unit = (OrderBookEvent => ()) + var marketdataObserver: (MarketDataEvent) => Unit = (MarketDataEvent => ()) + + def processOrderBookRequest(request: OrderBookRequest): Unit = request match { + case order: NewOrder => { + + val currentTime = System.currentTimeMillis + + val (isOK, message) = validateOrder(order) + + if (!isOK) this.transactionObserver(Rejected(currentTime, message.getOrElse("N/A"), order)) + else { + this.transactionObserver(Acknowledged(currentTime, order)) + + val orderBookOrder = Order(order.timestamp,order.tradeID,order.symbol,order.qty,order.isBuy,order.price,order) + processNewOrder(orderBookOrder) + } + } + case cancel:Cancel => { + val order = cancel.order + + val orderQ = if (order.isBuy) bidsQ else offersQ + + val isRemoved = orderQ.remove(order) + + if(isRemoved){ + this.transactionObserver(Acknowledged(System.currentTimeMillis(),cancel)) + updateBBO() + } + else this.transactionObserver(Rejected(System.currentTimeMillis(),"Order not found",cancel)) + } + case amend:Amend => { + val order = amend.order + val orderBookOrder = Order(order.timestamp,order.tradeID,order.symbol,order.qty,order.isBuy,order.price,order) + + val orderQ = if (order.isBuy) bidsQ else offersQ + val oppositeQ = if (order.isBuy) offersQ else bidsQ + + if(!orderQ.remove(orderBookOrder)){ + this.transactionObserver(Rejected(System.currentTimeMillis(),"Order not found",amend)) + } + else{ + + if(amend.newQty.isDefined) orderBookOrder.qty = amend.newQty.get + if(amend.newPrice.isDefined) orderBookOrder.price = amend.newPrice + + orderQ.add(orderBookOrder) + this.transactionObserver(Acknowledged(System.currentTimeMillis(),amend)) + updateBBO() + } + } + } + + def processNewOrder(orderBookOrder: Order) { + val currentTime = System.currentTimeMillis + + val orderQ = if (orderBookOrder.isBuy) bidsQ else offersQ + val oppositeQ = if (orderBookOrder.isBuy) offersQ else bidsQ + + + if (orderBookOrder.price.isDefined) { + //=====LIMIT ORDER===== + + if (oppositeQ.isEmpty || !isLimitOrderExecutable(orderBookOrder, oppositeQ.peek)) { + orderQ.add(orderBookOrder) + updateBBO() + } + else { + matchOrder(orderBookOrder, oppositeQ) + } + } + else { + //=====Market order===== + //TODO: what if order was already partially executed, replace reject with partial cancel? + if (oppositeQ.isEmpty) this.transactionObserver(Rejected(currentTime, "No opposing orders in queue", orderBookOrder.newOrderEvent)) + else matchOrder(orderBookOrder, oppositeQ) + } + } + + private def validateOrder(order: NewOrder): (Boolean, Option[String]) = (true, None) + + private def updateBBO() = { + val bidHead = Option(bidsQ.peek) + val offerHead = Option(offersQ.peek) + + if(bidHead != bestBid || offerHead != bestOffer){ + bestBid = bidHead + bestOffer = offerHead + + var bidPrice:Option[Double]=None + var bidQty:Option[Long]=None + var offerPrice:Option[Double]=None + var offerQty:Option[Long] = None + + //TODO: Does scala have some sort of monad magic to get rid of these, essentially, nested null checks? + if(bestBid.isDefined){ + bidPrice = bestBid.get.price + bidQty = Some(bestBid.get.qty) + } + if(bestOffer.isDefined){ + offerPrice = bestOffer.get.price + offerQty = Some(bestOffer.get.qty) + } + + this.marketdataObserver(BBOChange(System.currentTimeMillis, this.symbol, bidPrice, bidQty, offerPrice, offerQty)) + } + } + + private def isLimitOrderExecutable(order: Order, oppositeOrder: Order): Boolean = { + if (order.isBuy) order.price.get >= oppositeOrder.price.get + else order.price.get <= oppositeOrder.price.get + } + + private def matchOrder(order: Order, oppositeQ: PriorityQueue[Order]): Unit = { + val oppositeOrder = oppositeQ.peek + val currentTime = System.currentTimeMillis() + + if (order.qty < oppositeOrder.qty) { + oppositeOrder.qty = oppositeOrder.qty - order.qty + + this.volume += order.qty + + this.transactionObserver(Filled(currentTime, order.price.get, order.qty, Array(order.newOrderEvent, oppositeOrder.newOrderEvent))) + this.marketdataObserver(LastSalePrice(currentTime, order.symbol, order.price.get, order.qty, volume)) + updateBBO() + } + else if (order.qty > oppositeOrder.qty) { + oppositeQ.poll + val reducedQty = order.qty - oppositeOrder.qty + order.qty = reducedQty + + this.volume += order.qty + + this.transactionObserver(Filled(currentTime, order.price.get, order.qty, Array(order.newOrderEvent, oppositeOrder.newOrderEvent))) + this.marketdataObserver(LastSalePrice(currentTime, order.symbol, order.price.get, order.qty, volume)) + updateBBO() + + processNewOrder(order) + } + else { + //TODO: doing an '==' on doubles is a BAD idea! + oppositeQ.poll + + this.volume += order.qty + + this.transactionObserver(Filled(currentTime, order.price.get, order.qty, Array(order.newOrderEvent, oppositeOrder.newOrderEvent))) + this.marketdataObserver(LastSalePrice(currentTime, order.symbol, order.price.get, order.qty, volume)) + updateBBO() + } + } + + + + def listenForEvents(observer: (OrderBookResponse) => Unit): Unit = this.transactionObserver = observer + + def listenForMarketData(observer: (MarketDataEvent) => Unit): Unit = this.marketdataObserver = observer +} + + +object Main extends App { + val random = new scala.util.Random + + val msftBook = new OrderBook("MSFT") + + msftBook.listenForEvents((response) => { + response match { + case resp => println(resp) + } + }) + + msftBook.listenForMarketData((response) => { + response match { + case resp => println(resp) + } + }) + + + //one bid, only bidQ should be populated + val order1 = NewOrder(1, "1", "MSFT", 100, true, Some(50)) + msftBook.processOrderBookRequest(order1) + assert(!msftBook.bidsQ.isEmpty) + assert(msftBook.offersQ.isEmpty) + + //execute 50 shares of the order in bidsQ + val order2 = NewOrder(1, "2", "MSFT", 50, false, Some(50)) + msftBook.processOrderBookRequest(order2) + assert(msftBook.bidsQ.peek.qty == 50) + assert(msftBook.offersQ.isEmpty) + + //offer shares at a price where both bid and offer queues are populated with 50 shares + val order3 = NewOrder(1, "3", "MSFT", 50, false, Some(51)) + msftBook.processOrderBookRequest(order3) + assert(msftBook.bidsQ.peek.qty == 50) + assert(msftBook.offersQ.peek.qty == 50) + +} \ No newline at end of file