diff --git a/build.sbt b/build.sbt index 635ca36..46a7cc1 100644 --- a/build.sbt +++ b/build.sbt @@ -28,11 +28,9 @@ lazy val commonSettings = Seq( compileOrder := CompileOrder.ScalaThenJava ) - // Define additional testing configurations lazy val Functional = config("functional") extend Test - // finally define the full project build settings lazy val core = (project in file(".")). settings(commonSettings: _*). @@ -43,4 +41,4 @@ lazy val core = (project in file(".")). "org.scalatest" %% "scalatest" % "3.0.1" % "functional, test" ), parallelExecution in Functional := true - ) + ) \ No newline at end of file diff --git a/src/main/scala/org/economicsl/auctions/Price.scala b/src/main/scala/org/economicsl/auctions/Price.scala index 82bc241..d9e7069 100644 --- a/src/main/scala/org/economicsl/auctions/Price.scala +++ b/src/main/scala/org/economicsl/auctions/Price.scala @@ -27,6 +27,8 @@ case class Price(value: Currency) extends AnyVal { Price(value + that.value) } + def unary_- : Price = Price(-value) // todo consider making Price fully numeric to get this for free! + } diff --git a/src/main/scala/org/economicsl/auctions/Quantity.scala b/src/main/scala/org/economicsl/auctions/Quantity.scala index a2adf1d..d0fb8df 100644 --- a/src/main/scala/org/economicsl/auctions/Quantity.scala +++ b/src/main/scala/org/economicsl/auctions/Quantity.scala @@ -31,6 +31,8 @@ case class Quantity(value: Long) extends AnyVal { Quantity(value - that.value) } + def unary_- : Quantity = Quantity(-value) // todo consider making Quantity fully numeric to get this for free! + } diff --git a/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/FourHeapOrderBook.scala b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/FourHeapOrderBook.scala new file mode 100644 index 0000000..d29f407 --- /dev/null +++ b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/FourHeapOrderBook.scala @@ -0,0 +1,179 @@ +/* +Copyright 2017 EconomicSL + +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 org.economicsl.auctions.multiunit.orderbooks + +import org.economicsl.auctions.Tradable +import org.economicsl.auctions.multiunit.orders.{AskOrder, BidOrder} + + +class FourHeapOrderBook[K, T <: Tradable] private(matched: MatchedOrders[K, T], unMatched: UnMatchedOrders[K, T]) { + + require(matched.bidOrders.headOption.forall{ case (_, b1) => unMatched.bidOrders.headOption.forall{ case (_, b2) => b1.limit >= b2.limit } }) + + require(unMatched.askOrders.headOption.forall{ case (_, a1) => matched.askOrders.headOption.forall{ case (_, a2) => a1.limit >= a2.limit } }) + + /* + /** Add a new `AskOrder` to the `OrderBook`. + * + * @param key + * @param order + * @return + * @note adding a new `AskOrder` is non-trivial and there are several cases to consider. + */ + def + (key: K, order: AskOrder[T]): FourHeapOrderBook[K, T] = { + (unMatched.bidOrders.headOption, matched.askOrders.headOption) match { + case (Some((out, bidOrder)), Some((in, askOrder))) => + if (order.limit <= bidOrder.limit && askOrder.limit <= bidOrder.limit) { + val remainingUnMatched = unMatched - (out, bidOrder) + val (updatedMatched, additionalUnMatched) = matched + (key -> order, out -> bidOrder) + val default = new FourHeapOrderBook(updatedMatched, remainingUnMatched) + additionalUnMatched.fold(default)(orders => new FourHeapOrderBook(updatedMatched, remainingUnMatched.mergeWith(orders))) + } else if (order.limit <= askOrder.limit) { + val (updatedMatched, additionalUnMatched) = matched - (in, askOrder) // what happens if queue is empty? + updatedMatched + (key -> order, ???) additionalUnMatched.bidOrders + new FourHeapOrderBook(updatedMatched, unMatched.mergeWith(additionalUnMatched)) + ??? + } else { + new FourHeapOrderBook(matched, unMatched + (key, order)) + } + case (Some((out, bidOrder)), None) => + if (order.limit <= bidOrder.limit) { + val (updatedMatched, optionalUnMatched) = matched + (key -> order, out -> bidOrder) + optionalUnMatched match { + case Some(additionalUnMatched) => + new FourHeapOrderBook(updatedMatched, unMatched.mergeWith(additionalUnMatched)) + case None => + new FourHeapOrderBook(updatedMatched, unMatched) + } + } else { + new FourHeapOrderBook(matched, unMatched + (key, order)) + } + case (None, Some((in ,askOrder))) => + if (order.limit <= askOrder.limit) { + //val (updatedMatched, additionalUnMatched) = matched.swap(key -> order, in) + //new FourHeapOrderBook(updatedMatched, unMatched.mergeWith(additionalUnMatched)) + ??? + } else { + new FourHeapOrderBook(matched, unMatched + (key, order)) + } + case (None, None) => new FourHeapOrderBook(matched, unMatched + (key, order)) + } + } + + /** Add a new `BidOrder` to the `OrderBook`. + * + * @param key + * @param order + * @return + */ + def + (key: K, order: BidOrder[T]): FourHeapOrderBook[K, T] = { + (matched.bidOrders.headOption, unMatched.askOrders.headOption) match { + case (Some((in, bidOrder)), Some((out, askOrder))) => + if (order.limit >= askOrder.limit && bidOrder.limit >= askOrder.limit) { + val residualUnMatched = unMatched - out + val (updatedMatched, optionalUnMatched) = matched + (out -> askOrder, key -> order) + optionalUnMatched match { + case Some(additionalUnMatched) => + new FourHeapOrderBook(updatedMatched, residualUnMatched.mergeWith(additionalUnMatched)) + case None => + new FourHeapOrderBook(updatedMatched, residualUnMatched) + } + } else if (order.limit >= bidOrder.limit) { + //val (updatedMatched, additionalUnMatched) = matched.swap(key -> order, in) + //new FourHeapOrderBook(updatedMatched, unMatched.mergeWith(additionalUnMatched)) + ??? + } else { + new FourHeapOrderBook(matched, unMatched + (key, order)) + } + case (Some((in ,bidOrder)), None) => + if (order.limit >= bidOrder.limit) { + //val (updatedMatched, additionalUnMatched) = matched.swap(key -> order, in) + //new FourHeapOrderBook(updatedMatched, unMatched.mergeWith(additionalUnMatched)) + ??? + } else { + new FourHeapOrderBook(matched, unMatched + (key, order)) + } + case (None, Some((out, askOrder))) => + if (order.limit >= askOrder.limit) { + val (updatedMatched, optionalUnMatched) = matched + (out -> askOrder, key -> order) + optionalUnMatched match { + case Some(additionalUnMatched) => + new FourHeapOrderBook(updatedMatched, unMatched.mergeWith(additionalUnMatched)) + case None => + new FourHeapOrderBook(updatedMatched, unMatched) + } + } else { + new FourHeapOrderBook(matched, unMatched + (key, order)) + } + case (None, None) => new FourHeapOrderBook(matched, unMatched + (key, order)) + } + } + + def contains(key: K): Boolean = matched.contains(key) || unMatched.contains(key) + + def isEmpty: Boolean = matched.isEmpty && unMatched.isEmpty + + def nonEmpty: Boolean = matched.nonEmpty || unMatched.nonEmpty + + /*def - (key: K): FourHeapOrderBook[K, T] = { + if (matched.contains(key) && unMatched.contains(key)) { + val (residualMatched, additionalUnMatched) = matched - key + val residualUnMatched = unMatched - key + new FourHeapOrderBook(residualMatched, residualUnMatched.mergeWith(additionalUnMatched)) + } else if (matched.contains(key)) { + val (residualMatched, additionalUnMatched) = matched - key + new FourHeapOrderBook(residualMatched, unMatched.mergeWith(additionalUnMatched)) + } else { + val residualUnMatched = unMatched - key + new FourHeapOrderBook(matched, unMatched.mergeWith(residualUnMatched)) + } + } + + /** Update an existing `AskOrder`. + * + * @note If an `AskOrder[T]` associated with the `key` already exists in the `OrderBook`, then the existing order + * (or orders in the case that the previously submitted order was split) are removed from this `OrderBook` + * before the `order` is added to this `OrderBook`. + */ + def updated(key: K, order: AskOrder[T]): FourHeapOrderBook[K, T] = { + if (contains(key)) this - key + (key -> order) else this + (key -> order) + } + + /** Update an existing `BidOrder`. + * + * @note If an `AskOrder[T]` associated with the `key` already exists in the `OrderBook`, then the existing order + * (or orders in the case that the previously submitted order was split) are removed from this `OrderBook` + * before the `order` is added to this `OrderBook`. + */ + def updated(key: K, order: BidOrder[T]): FourHeapOrderBook[K, T] = { + if (contains(key)) this - key + (key -> order) else this + (key -> order) + }*/ + + */ +} + + +object FourHeapOrderBook { + + def empty[K, T <: Tradable](implicit askOrdering: Ordering[K], bidOrdering: Ordering[K]): FourHeapOrderBook[K, T] = { + val matchedOrders = MatchedOrders.empty[K, T](askOrdering.reverse, bidOrdering) + val unMatchedOrders = UnMatchedOrders.empty[K, T](askOrdering, bidOrdering.reverse) + new FourHeapOrderBook(matchedOrders, unMatchedOrders) + } + + +} + diff --git a/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/MatchedOrders.scala b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/MatchedOrders.scala new file mode 100644 index 0000000..fbbe30d --- /dev/null +++ b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/MatchedOrders.scala @@ -0,0 +1,102 @@ +/* +Copyright 2017 EconomicSL + +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 org.economicsl.auctions.multiunit.orderbooks + +import org.economicsl.auctions.{Quantity, Tradable} +import org.economicsl.auctions.multiunit.orders.{AskOrder, BidOrder} + + +protected[orderbooks] final case class MatchedOrders[K, T <: Tradable](askOrders: SortedAskOrders[K, T], + bidOrders: SortedBidOrders[K, T]) { + + /* Number of units supplied must equal the number of units demanded. */ + require(askOrders.numberUnits == bidOrders.numberUnits) + + /* Limit price of the first `BidOrder` must exceed the limit price of the first `AskOrder`. */ + require(bidOrders.headOption.forall{ case (_, bidOrder) => askOrders.headOption.forall{ case (_, askOrder) => bidOrder.limit >= askOrder.limit } }) + + val askOrdering: Ordering[K] = askOrders.ordering + + val bidOrdering: Ordering[K] = bidOrders.ordering + + /** Add a new pair of ask and bid orders into the MatchedOrders. + * + * @note Unless the quantities of the `AskOrder` and `BidOrder` match exactly, then the larger order must + * be rationed in order to maintain the invariant that the total quantity of supply matches the total quantity + * of demand. + */ + def + (kv1: (K, AskOrder[T]), kv2: (K, BidOrder[T])): (MatchedOrders[K, T], Option[UnMatchedOrders[K, T]]) = { + val excessDemand = kv2._2.quantity - kv1._2.quantity + if (excessDemand < Quantity(0)) { + val (matched, rationed) = (kv1._2.withQuantity(kv2._2.quantity), kv1._2.withQuantity(-excessDemand)) // split the askOrder into a matched and rationed component + val rationedOrders = UnMatchedOrders.empty[K, T](askOrders.ordering, bidOrders.ordering) + (MatchedOrders(askOrders + (kv1._1, matched), bidOrders + (kv2._1, kv2._2)), Some(rationedOrders + (kv1._1, rationed))) + } else if (excessDemand > Quantity(0)) { + val (matched, rationed) = (kv2._2.withQuantity(kv1._2.quantity), kv2._2.withQuantity(excessDemand)) // split the bidOrder into a matched and residual component + val rationedOrders = UnMatchedOrders.empty[K, T](askOrders.ordering, bidOrders.ordering) + (MatchedOrders(askOrders + (kv1._1, kv1._2), bidOrders + (kv2._1, matched)), Some(rationedOrders + (kv2._1, rationed))) + } else { + (MatchedOrders(askOrders + (kv1._1, kv1._2), bidOrders + (kv2._1, kv2._2)), None) + } + } + + def - (key: K, order: AskOrder[T]): (MatchedOrders[K, T], UnMatchedOrders[K, T]) = { + val remainingAskOrders = askOrders - (key, order) + val (matched, unMatched) = bidOrders.splitAt(order.quantity) + val empty = SortedAskOrders.empty[K, T](askOrdering) + (MatchedOrders(remainingAskOrders, matched), UnMatchedOrders(empty, unMatched)) + } + + def - (key: K, order: BidOrder[T]): (MatchedOrders[K, T], UnMatchedOrders[K, T]) = { + val remainingBidOrders = bidOrders - (key, order) + val (matched, unMatched) = askOrders.splitAt(order.quantity) + val empty = SortedBidOrders.empty[K, T](bidOrdering) + (MatchedOrders(matched, remainingBidOrders), UnMatchedOrders(unMatched, empty)) + } + + def contains(key: K): Boolean = askOrders.contains(key) || bidOrders.contains(key) + + def isEmpty: Boolean = askOrders.isEmpty && bidOrders.isEmpty + + def nonEmpty: Boolean = askOrders.nonEmpty && bidOrders.nonEmpty + + /* + def removeAndReplace(askOrder: (UUID, AskOrder[T]), bidOrder: (UUID, BidOrder[T])): MatchedOrders[K, T] = { + new MatchedOrders(askOrders - askOrder._1, bidOrders.updated(bidOrder._1, bidOrder._2)) + } + + def updated(askOrder: (UUID, AskOrder[T]), bidOrder: (UUID, BidOrder[T])): MatchedOrders[K, T] = { + new MatchedOrders(askOrders + askOrder, bidOrders + bidOrder) + } + */ + +} + + +object MatchedOrders { + + /** Create an instance of `MatchedOrders`. + * + * @return + * @note the heap used to store store the `AskOrder` instances is ordered from high to low + * based on `limit` price; the heap used to store store the `BidOrder` instances is + * ordered from low to high based on `limit` price. + */ + def empty[K, T <: Tradable](askOrdering: Ordering[K], bidOrdering: Ordering[K]): MatchedOrders[K, T] = { + new MatchedOrders(SortedAskOrders.empty(askOrdering), SortedBidOrders.empty(bidOrdering)) + } + +} diff --git a/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/OrderBook.scala b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/OrderBook.scala new file mode 100644 index 0000000..5c37d8f --- /dev/null +++ b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/OrderBook.scala @@ -0,0 +1,138 @@ +/* +Copyright 2017 EconomicSL + +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 org.economicsl.auctions.multiunit.orderbooks + +import org.economicsl.auctions.multiunit.orders.Order +import org.economicsl.auctions.{Quantity, Tradable} + +import scala.collection.generic.CanBuildFrom +import scala.collection.{GenIterable, immutable} + + +/** + * + * @param existing sorted mapping from keys to collections of orders sharing a particular key. + * @param numberUnits + * @tparam K + * @tparam O + */ +class OrderBook[K, O <: Order[_ <:Tradable], CC <: GenIterable[O]] private(existing: immutable.TreeMap[K, CC], + val numberUnits: Quantity) + (implicit cbf: CanBuildFrom[CC, O, CC]) { + + val ordering: Ordering[K] = existing.ordering + + /** Return a new `OrderBook` instance containing an additional `AskOrder` instance. + * + * @param key + * @param order + * @return + * @note if this `OrderBook` instance contains an existing collection of `AskOrder` instances sharing the same + * key as the new `AskOrder` instance, the new `AskOrder` instance is added to the existing collection. + */ + def + (key: K, order: O): OrderBook[K, O, CC] = { + val current = existing.getOrElse(key, empty) + val builder = cbf(current) + builder += order + new OrderBook(existing + (key -> builder.result()), numberUnits + order.quantity) + } + + /** Return a new `OrderBook` instance containing an collection of `AskOrder` instances. + * + * @param key + * @param orders + * @return + * @note if this `OrderBook` instance contains an existing collection of `AskOrder` instances sharing the same + * key, then the new collection replaces the existing collection. + */ + def + (key: K, orders: CC): OrderBook[K, O, CC] = { + val currentUnits = aggregate(existing.getOrElse(key, empty)) + val incomingUnits = aggregate(orders) + val change = currentUnits - incomingUnits + new OrderBook(existing + (key -> orders), numberUnits + change) + } + + def - (key: K): OrderBook[K, O, CC] = { + existing.get(key) match { + case Some(orders) => + val removedUnits = orders.foldLeft(Quantity(0))((total, order) => total + order.quantity) + new OrderBook(existing - key, numberUnits - removedUnits) + case None => this + } + } + + def - (key: K, order: O): OrderBook[K, O, CC] = { + existing.get(key) match { + case Some(orders) => + val builder = cbf() + orders.foreach(o => if (o != order) builder += o) + val residualOrders = builder.result() + if (residualOrders.isEmpty) { + new OrderBook(existing - key, numberUnits - order.quantity) + } else { + new OrderBook(existing + (key -> residualOrders), numberUnits - order.quantity) + } + case None => this + } + } + + def contains(key: K): Boolean = existing.contains(key) + + def foldLeft[B](z: B)(op: (B, (K, CC)) => B): B = { + existing.foldLeft(z)(op) + } + + def getOrElse(key: K, default: => CC): CC = { + existing.getOrElse(key, default) + } + + def headOption: Option[(K, O)] = { + existing.headOption.flatMap{ case (key, orders) => orders.headOption.map(order => (key, order)) } + } + + def isEmpty: Boolean = existing.isEmpty + + def mergeWith(other: OrderBook[K, O, CC]): OrderBook[K, O, CC] = { + other.foldLeft(this) { case (ob, (key, orders)) => + val current = ob.getOrElse(key, empty) + val builder = cbf(current) + orders.foreach(order => builder += order) + ob + (key, builder.result()) + } + } + + def nonEmpty: Boolean = existing.nonEmpty + + /* Use of GenIterable allows for use on both standard and parallel collections. */ + private[this] def aggregate(orders: GenIterable[O]): Quantity = { + orders.aggregate(Quantity(0))((total, order) => total + order.quantity, _ + _ ) + } + + private[this] def empty: CC = { + cbf().result() + } + +} + + +object OrderBook { + + def empty[K, O <: Order[_ <: Tradable], CC <: GenIterable[O]](implicit ordering: Ordering[K], cbf: CanBuildFrom[CC, O, CC]): OrderBook[K, O, CC] = { + val initial = immutable.TreeMap.empty(ordering) + new OrderBook[K, O, CC](initial, Quantity(0))(cbf) + } + +} \ No newline at end of file diff --git a/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/SortedAskOrders.scala b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/SortedAskOrders.scala new file mode 100644 index 0000000..e33fbf1 --- /dev/null +++ b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/SortedAskOrders.scala @@ -0,0 +1,148 @@ +/* +Copyright 2017 EconomicSL + +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 org.economicsl.auctions.multiunit.orderbooks + +import org.economicsl.auctions.multiunit.orders.AskOrder +import org.economicsl.auctions.{Quantity, Tradable} + +import scala.collection.immutable + + +/** + * + * @param existing sorted mapping from keys to collections of orders sharing a particular key. + * @param numberUnits + * @tparam K + * @tparam T + */ +class SortedAskOrders[K, T <: Tradable] private(existing: immutable.TreeMap[K, immutable.Queue[AskOrder[T]]], val numberUnits: Quantity) { + + val ordering: Ordering[K] = existing.ordering + + /** Return a new `SortedAskOrders` instance containing an additional `AskOrder` instance. + * + * @param key + * @param order + * @return + * @note if this `SortedAskOrders` instance contains an existing collection of `AskOrder` instances sharing the same + * key as the new `AskOrder` instance, the new `AskOrder` instance is added to the existing collection. + */ + def + (key: K, order: AskOrder[T]): SortedAskOrders[K, T] = { + val current = existing.getOrElse(key, empty) + new SortedAskOrders(existing + (key -> current.enqueue(order)), numberUnits + order.quantity) + } + + /** Return a new `SortedAskOrders` instance containing an collection of `AskOrder` instances. + * + * @param key + * @param orders + * @return + * @note if this `SortedAskOrders` instance contains an existing collection of `AskOrder` instances sharing the same + * key, then the new collection replaces the existing collection. + */ + def + (key: K, orders: immutable.Queue[AskOrder[T]]): SortedAskOrders[K, T] = { + val currentUnits = aggregate(existing.getOrElse(key, empty)) + val incomingUnits = aggregate(orders) + val change = currentUnits - incomingUnits + new SortedAskOrders(existing + (key -> orders), numberUnits + change) + } + + def - (key: K): SortedAskOrders[K, T] = { + existing.get(key) match { + case Some(orders) => + val removedUnits = orders.foldLeft(Quantity(0))((total, order) => total + order.quantity) + new SortedAskOrders(existing - key, numberUnits - removedUnits) + case None => this + } + } + + def - (key: K, order: AskOrder[T]): SortedAskOrders[K, T] = { + existing.get(key) match { + case Some(orders) => + val residualOrders = orders.diff(immutable.Queue(order)) // multi-set diff! + if (residualOrders.isEmpty) { + new SortedAskOrders(existing - key, numberUnits - order.quantity) + } else { + new SortedAskOrders(existing + (key -> residualOrders), numberUnits - order.quantity) + } + case None => this + } + } + + def contains(key: K): Boolean = existing.contains(key) + + def foldLeft[B](z: B)(op: (B, (K, immutable.Queue[AskOrder[T]])) => B): B = { + existing.foldLeft(z)(op) + } + + def getOrElse(key: K, default: => immutable.Queue[AskOrder[T]]): immutable.Queue[AskOrder[T]] = { + existing.getOrElse(key, default) + } + + def headOption: Option[(K, AskOrder[T])] = { + existing.headOption.flatMap{ case (key, orders) => orders.headOption.map(order => (key, order)) } + } + + def isEmpty: Boolean = existing.isEmpty + + def mergeWith(other: SortedAskOrders[K, T]): SortedAskOrders[K, T] = { + other.foldLeft(this) { case (ob, (key, orders)) => + val currentOrders = ob.getOrElse(key, empty) + ob + (key, currentOrders.enqueue(orders)) + } + } + + def nonEmpty: Boolean = existing.nonEmpty + + def splitAt(quantity: Quantity): (SortedAskOrders[K, T], SortedAskOrders[K, T]) = { + + @annotation.tailrec + def loop(prefix: SortedAskOrders[K, T], suffix: SortedAskOrders[K, T]): (SortedAskOrders[K, T], SortedAskOrders[K, T]) = { + suffix.headOption match { + case Some((key, askOrder)) => + val remainingQuantity = quantity - prefix.numberUnits + if (remainingQuantity > askOrder.quantity) { + loop(prefix + (key, askOrder), suffix - (key, askOrder)) + } else if (remainingQuantity < askOrder.quantity) { + (prefix + (key, askOrder.withQuantity(remainingQuantity)), suffix - (key, askOrder) + (key, askOrder.withQuantity(askOrder.quantity - remainingQuantity))) + } else { + (prefix + (key, askOrder), suffix - (key, askOrder)) + } + case None => + (prefix, SortedAskOrders.empty(ordering)) + } + } + loop(SortedAskOrders.empty[K, T](ordering), this) + + } + + private[this] def aggregate(orders: immutable.Queue[AskOrder[T]]): Quantity = { + orders.aggregate(Quantity(0))((total, order) => total + order.quantity, _ + _ ) + } + + private[this] def empty: immutable.Queue[AskOrder[T]] = immutable.Queue.empty[AskOrder[T]] + +} + + +object SortedAskOrders { + + def empty[K, T <: Tradable](implicit ordering: Ordering[K]): SortedAskOrders[K, T] = { + new SortedAskOrders(immutable.TreeMap.empty[K, immutable.Queue[AskOrder[T]]](ordering), Quantity(0)) + } + +} + diff --git a/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/SortedBidOrders.scala b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/SortedBidOrders.scala new file mode 100644 index 0000000..85044d5 --- /dev/null +++ b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/SortedBidOrders.scala @@ -0,0 +1,135 @@ +/* +Copyright 2017 EconomicSL + +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 org.economicsl.auctions.multiunit.orderbooks + +import org.economicsl.auctions.multiunit.orders.BidOrder +import org.economicsl.auctions.{Quantity, Tradable} + +import scala.collection.immutable + + +class SortedBidOrders[K, T <: Tradable] private(existing: immutable.TreeMap[K, immutable.Queue[BidOrder[T]]], val numberUnits: Quantity) { + + val ordering: Ordering[K] = existing.ordering + + /** Return a new `SortedBidOrders` instance containing an additional `BidOrder` instance. + * + * @param key + * @param order + * @return + * @note if this `SortedBidOrders` instance contains an existing collection of `BidOrder` instances sharing the same + * key as the new `BidOrder` instance, the new `BidOrder` instance is added to the existing collection. + */ + def + (key: K, order: BidOrder[T]): SortedBidOrders[K, T] = { + val current = existing.getOrElse(key, immutable.Queue.empty[BidOrder[T]]) + new SortedBidOrders(existing + (key -> current.enqueue(order)), numberUnits + order.quantity) + } + + /** Return a new `SortedBidOrders` instance containing an collection of `BidOrder` instances. + * + * @param key + * @param orders + * @return + * @note if this `SortedBidOrders` instance contains an existing collection of `BidOrder` instances sharing the same + * key, then the new collection replaces the existing collection. + */ + def + (key: K, orders: immutable.Queue[BidOrder[T]]): SortedBidOrders[K, T] = { + val currentOrders = existing.getOrElse(key, immutable.Queue.empty[BidOrder[T]]) + val currentUnits = currentOrders.foldLeft(Quantity(0)){ case (total, order) => total + order.quantity } + val incomingUnits = orders.foldLeft(Quantity(0)){ case (total, order) => total + order.quantity } + val netChange: Quantity = currentUnits - incomingUnits + new SortedBidOrders(existing + (key -> orders), numberUnits + netChange) + } + + def - (key: K): SortedBidOrders[K, T] = { + existing.get(key) match { + case Some(orders) => + val removedUnits = orders.foldLeft(Quantity(0))((total, order) => total + order.quantity) + new SortedBidOrders(existing - key, numberUnits - removedUnits) + case None => this + } + } + + def - (key: K, order: BidOrder[T]): SortedBidOrders[K, T] = { + existing.get(key) match { + case Some(orders) => + val residualOrders = orders.diff(immutable.Queue(order)) // multi-set diff available on SeqLike collections! + if (residualOrders.isEmpty) { + new SortedBidOrders(existing - key, numberUnits - order.quantity) + } else { + new SortedBidOrders(existing + (key -> residualOrders), numberUnits - order.quantity) + } + case None => this + } + } + + def contains(key: K): Boolean = existing.contains(key) + + def foldLeft[B](z: B)(op: (B, (K, immutable.Queue[BidOrder[T]])) => B): B = { + existing.foldLeft(z)(op) + } + + def getOrElse(key: K, default: => immutable.Queue[BidOrder[T]]): immutable.Queue[BidOrder[T]] = { + existing.getOrElse(key, default) + } + + def headOption: Option[(K, BidOrder[T])] = { + existing.headOption.flatMap{ case (key, orders) => orders.headOption.map(order => (key, order)) } + } + + def isEmpty: Boolean = existing.isEmpty + + def mergeWith(other: SortedBidOrders[K, T]): SortedBidOrders[K, T] = { + other.foldLeft(this) { case (ob, (key, orders)) => + val currentOrders = ob.getOrElse(key, immutable.Queue.empty[BidOrder[T]]) + ob + (key, currentOrders.enqueue(orders)) + } + } + + def nonEmpty: Boolean = existing.nonEmpty + + def splitAt(quantity: Quantity): (SortedBidOrders[K, T], SortedBidOrders[K, T]) = { + + @annotation.tailrec + def loop(prefix: SortedBidOrders[K, T], suffix: SortedBidOrders[K, T]): (SortedBidOrders[K, T], SortedBidOrders[K, T]) = { + suffix.headOption match { + case Some((key, bidOrder)) => + val remainingQuantity = quantity - prefix.numberUnits + if (remainingQuantity > bidOrder.quantity) { + loop(prefix + (key, bidOrder), suffix - (key, bidOrder)) + } else if (remainingQuantity < bidOrder.quantity) { + (prefix + (key, bidOrder.withQuantity(remainingQuantity)), suffix - (key, bidOrder) + (key, bidOrder.withQuantity(bidOrder.quantity - remainingQuantity))) + } else { + (prefix + (key, bidOrder), suffix - (key, bidOrder)) + } + case None => + (prefix, SortedBidOrders.empty[K, T](ordering)) + } + } + loop(SortedBidOrders.empty[K, T](ordering), this) + + } + +} + + +object SortedBidOrders { + + def empty[K, T <: Tradable](implicit ordering: Ordering[K]): SortedBidOrders[K, T] = { + new SortedBidOrders(immutable.TreeMap.empty[K, immutable.Queue[BidOrder[T]]](ordering), Quantity(0)) + } + +} diff --git a/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/UnMatchedOrders.scala b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/UnMatchedOrders.scala new file mode 100644 index 0000000..8d74b68 --- /dev/null +++ b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/UnMatchedOrders.scala @@ -0,0 +1,84 @@ +/* +Copyright 2017 EconomicSL + +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 org.economicsl.auctions.multiunit.orderbooks + +import org.economicsl.auctions.Tradable +import org.economicsl.auctions.multiunit.orders.{AskOrder, BidOrder} + + +protected[orderbooks] final case class UnMatchedOrders[K, T <: Tradable](askOrders: SortedAskOrders[K, T], + bidOrders: SortedBidOrders[K, T]) { + + /* Limit price of "best" `BidOrder` instance must not exceed the limit price of the "best" `AskOrder` instance. */ + require(bidOrders.headOption.forall{ case (_, bidOrder) => askOrders.headOption.forall{ case (_, askOrder) => bidOrder.limit <= askOrder.limit } }) + + def + (key: K, order: AskOrder[T]): UnMatchedOrders[K, T] = { + new UnMatchedOrders(askOrders + (key, order), bidOrders) + } + + def + (key: K, order: BidOrder[T]): UnMatchedOrders[K, T] = { + new UnMatchedOrders(askOrders, bidOrders + (key, order)) + } + + /** Remove a collection of order from the collection of unmatched orders. */ + def - (key: K): UnMatchedOrders[K, T] = { + if (askOrders.contains(key)) { + new UnMatchedOrders(askOrders - key, bidOrders) + } else { + new UnMatchedOrders(askOrders, bidOrders - key) + } + } + + def - (key: K, order: AskOrder[T]): UnMatchedOrders[K, T] = { + val remainingAskOrders = askOrders - (key, order) + UnMatchedOrders(remainingAskOrders, bidOrders) + } + + def - (key: K, order: BidOrder[T]): UnMatchedOrders[K, T] = { + val remainingBidOrders = bidOrders - (key, order) + UnMatchedOrders(askOrders, remainingBidOrders) + } + + /** Check whether an order is contained in the collection of unmatched orders using. */ + def contains(key: K): Boolean = askOrders.contains(key) || bidOrders.contains(key) + + def isEmpty: Boolean = askOrders.isEmpty && bidOrders.isEmpty + + def nonEmpty: Boolean = askOrders.nonEmpty || bidOrders.nonEmpty + + def mergeWith(other: UnMatchedOrders[K, T]): UnMatchedOrders[K, T] = { + new UnMatchedOrders(askOrders.mergeWith(other.askOrders), bidOrders.mergeWith(bidOrders)) + } + +} + + +object UnMatchedOrders { + + /** Create an instance of `UnMatchedOrders`. + * + * @param askOrdering + * @param bidOrdering + * @return + * @note the heap used to store store the `AskOrder` instances is ordered from low to high + * based on `limit` price; the heap used to store store the `BidOrder` instances is + * ordered from high to low based on `limit` price. + */ + def empty[K, T <: Tradable](implicit askOrdering: Ordering[K], bidOrdering: Ordering[K]): UnMatchedOrders[K, T] = { + new UnMatchedOrders(SortedAskOrders.empty(askOrdering), SortedBidOrders.empty(bidOrdering)) + } + +} diff --git a/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/package.scala b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/package.scala new file mode 100644 index 0000000..a01f59a --- /dev/null +++ b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/package.scala @@ -0,0 +1,31 @@ +/* +Copyright 2017 EconomicSL + +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 org.economicsl.auctions.multiunit + +import org.economicsl.auctions.multiunit.orders.Order +import org.economicsl.auctions.{Quantity, Tradable} + +import scala.collection.GenIterable + + +/** Documentation for multi-unit orderbooks goes here! */ +package object orderbooks { + + def totalQuantity[T <: Tradable](orders: GenIterable[Order[T]]): Quantity = { + orders.aggregate[Quantity](Quantity(0))((total, order) => Quantity(total.value + order.quantity.value), (q1, q2) => Quantity(q1.value + q2.value)) + } + +} diff --git a/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/sandbox.sc b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/sandbox.sc new file mode 100644 index 0000000..41787fa --- /dev/null +++ b/src/main/scala/org/economicsl/auctions/multiunit/orderbooks/sandbox.sc @@ -0,0 +1,13 @@ +import org.economicsl.auctions.{Currency, Price, Tradable} +import org.economicsl.auctions.multiunit.orderbooks.OrderBook +import org.economicsl.auctions.multiunit.orders.{AskOrder, BidOrder} + +import scala.collection.immutable + +case class GoogleStock(tick: Currency = 1) extends Tradable + +val ob = OrderBook.empty[Price, AskOrder[GoogleStock], immutable.Queue[AskOrder[GoogleStock]]] + +val ob2 = OrderBook.empty[Price, AskOrder[GoogleStock], immutable.Set[AskOrder[GoogleStock]]] + +val ob3 = OrderBook.empty[Price, BidOrder[GoogleStock], immutable.List[BidOrder[GoogleStock]]] diff --git a/src/main/scala/org/economicsl/auctions/multiunit/LimitAskOrder.scala b/src/main/scala/org/economicsl/auctions/multiunit/orders/LimitAskOrder.scala similarity index 70% rename from src/main/scala/org/economicsl/auctions/multiunit/LimitAskOrder.scala rename to src/main/scala/org/economicsl/auctions/multiunit/orders/LimitAskOrder.scala index fa79751..b2b6241 100644 --- a/src/main/scala/org/economicsl/auctions/multiunit/LimitAskOrder.scala +++ b/src/main/scala/org/economicsl/auctions/multiunit/orders/LimitAskOrder.scala @@ -13,11 +13,11 @@ 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 org.economicsl.auctions.multiunit +package org.economicsl.auctions.multiunit.orders import java.util.UUID -import org.economicsl.auctions.{Price, Quantity, SinglePricePoint, Tradable} +import org.economicsl.auctions.{Price, Quantity, Tradable} /** An order to sell multiple units of a tradable at a per-unit price greater than or equal to the limit price. @@ -30,8 +30,14 @@ import org.economicsl.auctions.{Price, Quantity, SinglePricePoint, Tradable} * @author davidrpugh * @since 0.1.0 */ -class LimitAskOrder[+T <: Tradable](val issuer: UUID, val limit: Price, val quantity: Quantity, val tradable: T) - extends AskOrder[T] with SinglePricePoint[T] +case class LimitAskOrder[+T <: Tradable](issuer: UUID, limit: Price, quantity: Quantity, tradable: T) + extends AskOrder[T] { + + def withQuantity(quantity: Quantity): LimitAskOrder[T] = { + copy(quantity = quantity) + } + +} /** Companion object for `LimitAskOrder`. @@ -43,10 +49,8 @@ class LimitAskOrder[+T <: Tradable](val issuer: UUID, val limit: Price, val quan */ object LimitAskOrder { - implicit def ordering[O <: LimitAskOrder[_ <: Tradable]]: Ordering[O] = SinglePricePoint.ordering[O] - - def apply[T <: Tradable](issuer: UUID, limit: Price, quantity: Quantity, tradable: T): LimitAskOrder[T] = { - new LimitAskOrder[T](issuer, limit, quantity, tradable) + implicit def ordering[O <: LimitAskOrder[_ <: Tradable]]: Ordering[(UUID, O)] = { + Ordering.by{case (uuid, order) => (order.limit, uuid) } } } diff --git a/src/main/scala/org/economicsl/auctions/multiunit/LimitBidOrder.scala b/src/main/scala/org/economicsl/auctions/multiunit/orders/LimitBidOrder.scala similarity index 77% rename from src/main/scala/org/economicsl/auctions/multiunit/LimitBidOrder.scala rename to src/main/scala/org/economicsl/auctions/multiunit/orders/LimitBidOrder.scala index 7cfca3a..64913bb 100644 --- a/src/main/scala/org/economicsl/auctions/multiunit/LimitBidOrder.scala +++ b/src/main/scala/org/economicsl/auctions/multiunit/orders/LimitBidOrder.scala @@ -13,7 +13,7 @@ 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 org.economicsl.auctions.multiunit +package org.economicsl.auctions.multiunit.orders import java.util.UUID @@ -30,8 +30,14 @@ import org.economicsl.auctions.{Price, Quantity, SinglePricePoint, Tradable} * @author davidrpugh * @since 0.1.0 */ -class LimitBidOrder[+T <: Tradable](val issuer: UUID, val limit: Price, val quantity: Quantity, val tradable: T) - extends BidOrder[T] with SinglePricePoint[T] +case class LimitBidOrder[+T <: Tradable](issuer: UUID, limit: Price, quantity: Quantity, tradable: T) + extends BidOrder[T] { + + def withQuantity(quantity: Quantity): LimitBidOrder[T] = { + copy(quantity = quantity) + } + +} /** Companion object for `LimitBidOrder`. @@ -45,9 +51,5 @@ object LimitBidOrder { implicit def ordering[O <: LimitBidOrder[_ <: Tradable]]: Ordering[O] = SinglePricePoint.ordering[O] - def apply[T <: Tradable](issuer: UUID, limit: Price, quantity: Quantity, tradable: T): LimitBidOrder[T] = { - new LimitBidOrder[T](issuer, limit, quantity, tradable) - } - } diff --git a/src/main/scala/org/economicsl/auctions/multiunit/MarketAskOrder.scala b/src/main/scala/org/economicsl/auctions/multiunit/orders/MarketAskOrder.scala similarity index 62% rename from src/main/scala/org/economicsl/auctions/multiunit/MarketAskOrder.scala rename to src/main/scala/org/economicsl/auctions/multiunit/orders/MarketAskOrder.scala index c699cbf..a949674 100644 --- a/src/main/scala/org/economicsl/auctions/multiunit/MarketAskOrder.scala +++ b/src/main/scala/org/economicsl/auctions/multiunit/orders/MarketAskOrder.scala @@ -13,11 +13,11 @@ 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 org.economicsl.auctions.multiunit +package org.economicsl.auctions.multiunit.orders import java.util.UUID -import org.economicsl.auctions.{Price, Quantity, SinglePricePoint, Tradable} +import org.economicsl.auctions.{Price, Quantity, Tradable} /** An order to sell a multiple units of a tradable at any positive price. @@ -29,23 +29,12 @@ import org.economicsl.auctions.{Price, Quantity, SinglePricePoint, Tradable} * @author davidrpugh * @since 0.1.0 */ -class MarketAskOrder[+T <: Tradable](val issuer: UUID, val quantity: Quantity, val tradable: T) - extends AskOrder[T] with SinglePricePoint[T] { +case class MarketAskOrder[+T <: Tradable](issuer: UUID, quantity: Quantity, tradable: T) extends AskOrder[T] { val limit: Price = Price.MinValue -} - - -/** Companion object for `MarketAskOrder`. - * - * @author davidrpugh - * @since 0.1.0 - */ -object MarketAskOrder { - - def apply[T <: Tradable](issuer: UUID, quantity: Quantity, tradable: T): MarketAskOrder[T] = { - new MarketAskOrder[T](issuer, quantity, tradable) + def withQuantity(quantity: Quantity): MarketAskOrder[T] = { + copy(quantity = quantity) } } diff --git a/src/main/scala/org/economicsl/auctions/multiunit/MarketBidOrder.scala b/src/main/scala/org/economicsl/auctions/multiunit/orders/MarketBidOrder.scala similarity index 62% rename from src/main/scala/org/economicsl/auctions/multiunit/MarketBidOrder.scala rename to src/main/scala/org/economicsl/auctions/multiunit/orders/MarketBidOrder.scala index d87dc6f..223eb28 100644 --- a/src/main/scala/org/economicsl/auctions/multiunit/MarketBidOrder.scala +++ b/src/main/scala/org/economicsl/auctions/multiunit/orders/MarketBidOrder.scala @@ -13,11 +13,11 @@ 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 org.economicsl.auctions.multiunit +package org.economicsl.auctions.multiunit.orders import java.util.UUID -import org.economicsl.auctions.{Price, Quantity, SinglePricePoint, Tradable} +import org.economicsl.auctions.{Price, Quantity, Tradable} /** An order to buy multiple units of a tradable at any positive price. @@ -29,23 +29,12 @@ import org.economicsl.auctions.{Price, Quantity, SinglePricePoint, Tradable} * @author davidrpugh * @since 0.1.0 */ -class MarketBidOrder[+T <: Tradable](val issuer: UUID, val quantity: Quantity, val tradable: T) - extends BidOrder[T] with SinglePricePoint[T] { +case class MarketBidOrder[+T <: Tradable](issuer: UUID, quantity: Quantity, tradable: T) extends BidOrder[T] { val limit: Price = Price.MaxValue -} - - -/** Companion object for `MarketBidOrder`. - * - * @author davidrpugh - * @since 0.1.0 - */ -object MarketBidOrder { - - def apply[T <: Tradable](issuer: UUID, quantity: Quantity, tradable: T): MarketBidOrder[T] = { - new MarketBidOrder[T](issuer, quantity, tradable) + def withQuantity(quantity: Quantity): MarketBidOrder[T] = { + copy(quantity = quantity) } } diff --git a/src/main/scala/org/economicsl/auctions/multiunit/Order.scala b/src/main/scala/org/economicsl/auctions/multiunit/orders/Order.scala similarity index 75% rename from src/main/scala/org/economicsl/auctions/multiunit/Order.scala rename to src/main/scala/org/economicsl/auctions/multiunit/orders/Order.scala index 0308970..b357bf8 100644 --- a/src/main/scala/org/economicsl/auctions/multiunit/Order.scala +++ b/src/main/scala/org/economicsl/auctions/multiunit/orders/Order.scala @@ -13,9 +13,9 @@ 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 org.economicsl.auctions.multiunit +package org.economicsl.auctions.multiunit.orders -import org.economicsl.auctions.{Contract, OrderLike, Tradable} +import org.economicsl.auctions._ /** Base trait for all multi-unit orders. @@ -24,7 +24,11 @@ import org.economicsl.auctions.{Contract, OrderLike, Tradable} * @author davidrpugh * @since 0.1.0 */ -sealed trait Order[+T <: Tradable] extends Contract with OrderLike[T] +sealed trait Order[+T <: Tradable] extends Contract with OrderLike[T] with SinglePricePoint[T] { + + def withQuantity(quantity: Quantity): Order[T] + +} /** Base trait for all multi-unit orders to sell a particular `Tradable`. @@ -33,7 +37,11 @@ sealed trait Order[+T <: Tradable] extends Contract with OrderLike[T] * @author davidrpugh * @since 0.1.0 */ -trait AskOrder[+T <: Tradable] extends Order[T] +trait AskOrder[+T <: Tradable] extends Order[T] { + + def withQuantity(quantity: Quantity): AskOrder[T] + +} /** Base trait for all multi-unit orders to buy a particular `Tradable` @@ -42,4 +50,8 @@ trait AskOrder[+T <: Tradable] extends Order[T] * @author davidrpugh * @since 0.1.0 */ -trait BidOrder[+T <: Tradable] extends Order[T] \ No newline at end of file +trait BidOrder[+T <: Tradable] extends Order[T] { + + def withQuantity(quantity: Quantity): BidOrder[T] + +} \ No newline at end of file diff --git a/src/main/scala/org/economicsl/auctions/multiunit/orders/package.scala b/src/main/scala/org/economicsl/auctions/multiunit/orders/package.scala new file mode 100644 index 0000000..c0fd62a --- /dev/null +++ b/src/main/scala/org/economicsl/auctions/multiunit/orders/package.scala @@ -0,0 +1,19 @@ +/* +Copyright (c) 2017 KAPSARC + +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 org.economicsl.auctions.multiunit + + +package object orders diff --git a/src/test/scala/org/economicsl/auctions/multiunit/orderbooks/SortedAskOrdersSpec.scala b/src/test/scala/org/economicsl/auctions/multiunit/orderbooks/SortedAskOrdersSpec.scala new file mode 100644 index 0000000..39865e5 --- /dev/null +++ b/src/test/scala/org/economicsl/auctions/multiunit/orderbooks/SortedAskOrdersSpec.scala @@ -0,0 +1,47 @@ +package org.economicsl.auctions.multiunit.orderbooks + +import java.util.UUID + +import org.economicsl.auctions.multiunit.orders.LimitAskOrder +import org.economicsl.auctions.{GoogleStock, Price, Quantity} +import org.scalatest.{FlatSpec, Matchers} + + +class SortedAskOrdersSpec extends FlatSpec with Matchers { + + "A SortedAskOrderBook" should "be able to add a new ask order" in { + + // Create a multi-unit limit ask order + val issuer = UUID.randomUUID() + val google = GoogleStock(tick = 1) + val order = LimitAskOrder(issuer, Price(10), Quantity(100), google) + + // Create an empty order book and add the order + val empty = SortedAskOrders.empty[(Price, UUID), GoogleStock] + empty.numberUnits should be(Quantity(0)) + empty.contains((order.limit, order.issuer)) should be(false) + + val nonEmpty = empty + ((order.limit, order.issuer), order) + nonEmpty.headOption should be (Some(((order.limit, order.issuer), order))) + nonEmpty.numberUnits should be(Quantity(100)) + nonEmpty.contains((order.limit, order.issuer)) should be(true) + + } + + "A SortedAskOrderBook" should "be able to remove an existing ask order" in { + + // Create a multi-unit limit ask order + val issuer = UUID.randomUUID() + val google = GoogleStock(tick = 1) + val order = LimitAskOrder(issuer, Price(10), Quantity(100), google) + + // Create an empty order book, add the order, and then remove it + val empty = SortedAskOrders.empty[(Price, UUID), GoogleStock] + val nonEmpty = empty + ((order.limit, order.issuer), order) + val withoutOrders = nonEmpty - ((order.limit, order.issuer), order) + withoutOrders.isEmpty should be(true) + + } + + +} diff --git a/src/test/scala/org/economicsl/auctions/multiunit/orderbooks/SortedBidOrdersSpec.scala b/src/test/scala/org/economicsl/auctions/multiunit/orderbooks/SortedBidOrdersSpec.scala new file mode 100644 index 0000000..7903f46 --- /dev/null +++ b/src/test/scala/org/economicsl/auctions/multiunit/orderbooks/SortedBidOrdersSpec.scala @@ -0,0 +1,47 @@ +package org.economicsl.auctions.multiunit.orderbooks + +import java.util.UUID + +import org.economicsl.auctions.multiunit.orders.LimitBidOrder +import org.economicsl.auctions.{GoogleStock, Price, Quantity} +import org.scalatest.{FlatSpec, Matchers} + + +class SortedBidOrdersSpec extends FlatSpec with Matchers { + + "A SortedBidOrderBook" should "be able to add a new ask order" in { + + // Create a multi-unit limit ask order + val issuer = UUID.randomUUID() + val google = GoogleStock(tick = 1) + val order = LimitBidOrder(issuer, Price(10), Quantity(100), google) + + // Create an empty order book and add the order + val empty = SortedBidOrders.empty[(Price, UUID), GoogleStock] + empty.numberUnits should be(Quantity(0)) + empty.contains((order.limit, order.issuer)) should be(false) + + val nonEmpty = empty + ((order.limit, order.issuer), order) + nonEmpty.headOption should be (Some(((order.limit, order.issuer), order))) + nonEmpty.numberUnits should be(Quantity(100)) + nonEmpty.contains((order.limit, order.issuer)) should be(true) + + } + + "A SortedBidOrderBook" should "be able to remove an existing ask order" in { + + // Create a multi-unit limit ask order + val issuer = UUID.randomUUID() + val google = GoogleStock(tick = 1) + val order = LimitBidOrder(issuer, Price(10), Quantity(100), google) + + // Create an empty order book, add the order, and then remove it + val empty = SortedBidOrders.empty[(Price, UUID), GoogleStock] + val nonEmpty = empty + ((order.limit, order.issuer), order) + val withoutOrders = nonEmpty - ((order.limit, order.issuer), order) + withoutOrders.isEmpty should be(true) + + } + + +}