Skip to content

Commit

Permalink
cleans up project structure
Browse files Browse the repository at this point in the history
  • Loading branch information
falconair committed Dec 28, 2014
1 parent 260eb5d commit 81d6694
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 26 deletions.
18 changes: 12 additions & 6 deletions .classpath
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src/main/scala"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="org.scala-ide.sdt.launching.SCALA_CONTAINER"/>
<classpathentry kind="output" path="target/scala-2.11/classes"/>
</classpath>
<classpathentry output="target/scala-2.11/classes" kind="src" path="src/main/scala"/>
<classpathentry output="target/scala-2.11/classes" kind="src" path="src/main/java"/>
<classpathentry output="target/scala-2.11/test-classes" kind="src" path="src/test/scala"/>
<classpathentry output="target/scala-2.11/test-classes" kind="src" path="src/test/java"/>
<classpathentry kind="con" path="org.scala-ide.sdt.launching.SCALA_CONTAINER"/>
<classpathentry kind="lib" path="/Users/shahbaz/.ivy2/cache/com.typesafe.akka/akka-actor_2.11/jars/akka-actor_2.11-2.3.8.jar"/>
<classpathentry kind="lib" path="/Users/shahbaz/.ivy2/cache/com.typesafe/config/bundles/config-1.2.1.jar"/>
<classpathentry kind="lib" path="/Users/shahbaz/.ivy2/cache/org.scalatest/scalatest_2.11/bundles/scalatest_2.11-2.2.1.jar"/>
<classpathentry kind="lib" path="/Users/shahbaz/.ivy2/cache/org.scala-lang.modules/scala-xml_2.11/bundles/scala-xml_2.11-1.0.2.jar"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="output" path="bin"/>
</classpath>
29 changes: 12 additions & 17 deletions .project
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>SimpleFinancialExchange</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.scala-ide.sdt.core.scalabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.scala-ide.sdt.core.scalanature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
<name>SimpleFinancialExchange</name>
<buildSpec>
<buildCommand>
<name>org.scala-ide.sdt.core.scalabuilder</name>
</buildCommand>
</buildSpec>
<natures>
<nature>org.scala-ide.sdt.core.scalanature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
<linkedResources> </linkedResources>
</projectDescription>
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ scalaVersion := "2.11.4"

libraryDependencies += "org.scalatest" % "scalatest_2.11" % "2.2.1" % "test"

libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.3.8"
libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.3.8"

1 change: 0 additions & 1 deletion project/build.properties

This file was deleted.

3 changes: 2 additions & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
logLevel := Level.Warn
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0")

248 changes: 248 additions & 0 deletions src/OrderBook.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import akka.actor.Actor
import akka.actor.Props
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) extends Actor{
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)

}

0 comments on commit 81d6694

Please sign in to comment.