From 7ed0ff9071108f706638b4266f76ceab2f84ee76 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Sun, 8 Jul 2018 15:41:37 -0400 Subject: [PATCH 1/5] some large-ish refactoring --- docker-compose.yml | 13 +- src/skirmish/cli/exchange.clj | 4 +- src/skirmish/core.clj | 8 +- src/skirmish/{ => exchange}/exchange.clj | 10 +- src/skirmish/{ => exchange}/match.clj | 2 +- src/skirmish/rest/core.clj | 2 +- src/skirmish/rest/exchange.clj | 3 +- .../skirmish/{ => exchange}/exchange_test.clj | 10 +- test/skirmish/exchange/match_test.clj | 116 ++++++++++++++++ test/skirmish/match_test.clj | 124 ------------------ 10 files changed, 147 insertions(+), 145 deletions(-) rename src/skirmish/{ => exchange}/exchange.clj (93%) rename src/skirmish/{ => exchange}/match.clj (99%) rename test/skirmish/{ => exchange}/exchange_test.clj (93%) create mode 100644 test/skirmish/exchange/match_test.clj delete mode 100644 test/skirmish/match_test.clj diff --git a/docker-compose.yml b/docker-compose.yml index 0f13115..6602f56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -154,7 +154,7 @@ services: skirmish-api: image: skirmish - command: ["lein", "run", "webserver"] + command: ["lein", "run", "api-server"] depends_on: - kafka-topics-ui # basically, we need all kafka environment: @@ -162,3 +162,14 @@ services: KAFKA_BOOTSTRAP_SERVERS: "kafka1:9092" ports: - "1602:1602" + + skirmish-exchange-core: + image: skirmish + command: ["lein", "run", "exchange-core"] + depends_on: + - skirmish-api # basically, we need all kafka + environment: + PORT: 1602 # https://en.wikipedia.org/wiki/Euronext_Amsterdam + KAFKA_BOOTSTRAP_SERVERS: "kafka1:9092" + ports: + - "1602:1602" diff --git a/src/skirmish/cli/exchange.clj b/src/skirmish/cli/exchange.clj index 7d413ff..7008121 100644 --- a/src/skirmish/cli/exchange.clj +++ b/src/skirmish/cli/exchange.clj @@ -1,7 +1,7 @@ (ns skirmish.cli.exchange - (:require [skirmish.match :refer [sell?]] + (:require [skirmish.exchange.match :refer [sell?]] [skirmish.util :refer :all] - [skirmish.exchange :refer [empty-book create-exchange-for-security]] + [skirmish.exchange.exchange :refer [empty-book create-exchange-for-security]] [skirmish.order] ;; for spec side effect [slingshot.slingshot :refer [try+ throw+]] [clojure.edn :as edn] diff --git a/src/skirmish/core.clj b/src/skirmish/core.clj index e1be0fa..9b4a57c 100644 --- a/src/skirmish/core.clj +++ b/src/skirmish/core.clj @@ -2,13 +2,13 @@ (:require [skirmish.cli.core] [skirmish.rest.core] [skirmish.cli.benchmark] - [skirmish.kafka.exchange-simulator] [aero.core :refer (read-config)]) (:gen-class)) -(def apps {:webserver skirmish.rest.core/-main - :benchmark skirmish.cli.benchmark/-main - :kafka-exchange-simulator skirmish.kafka.exchange-simulator/-main}) +(def apps {:api-server skirmish.rest.core/api-main + :exchange-core skirmish.rest.core/api-main ;; should be skirmish.exchange.core/api-main + :benchmark skirmish.cli.benchmark/-main}) + (defn -main [& argv] (let [config (-> "config.edn" clojure.java.io/resource read-config) diff --git a/src/skirmish/exchange.clj b/src/skirmish/exchange/exchange.clj similarity index 93% rename from src/skirmish/exchange.clj rename to src/skirmish/exchange/exchange.clj index 1bd8a02..72c1450 100644 --- a/src/skirmish/exchange.clj +++ b/src/skirmish/exchange/exchange.clj @@ -1,8 +1,8 @@ -(ns skirmish.exchange - (:require [skirmish.match :refer [sell? - create-buy-book - create-sell-book - match-order-in-book]] +(ns skirmish.exchange.exchange + (:require [skirmish.exchange.match :refer [sell? + create-buy-book + create-sell-book + match-order-in-book]] [skirmish.util :refer :all] [clojure.core.async :refer [chan go >! empty? + trades => [#:book.order{:quantity 10 :price 2 :book-id 101 :order-id 102 :type :sell}])) + + (fact "Simplest case II." + (let [sell-book (create-sell-book {:type :sell :quantity 10 :price 2 :book.order/id 103}) + order {:type :buy :quantity 10 :price 3 :book.order/id 104} + [trades new-book] (match-order-in-book sell-book order)] + new-book => empty? + trades => [#:book.order{:quantity 10 :price 2 :book-id 103 :order-id 104 :type :sell}])) + (fact "Simplest case III." + (let [sell-book (create-sell-book {:type :sell :quantity 10 :price 2 :book.order/id 105}) + order {:type :buy :quantity 8 :price 2 :book.order/id 106} + [trades new-book] (match-order-in-book sell-book order)] + new-book => [{:type :sell :quantity 2 :price 2 :book.order/id 105}] + trades => [#:book.order{:quantity 8 :price 2 :book-id 105 :order-id 106 :type :sell}])) + (fact "Simplest case IV." + (let [sell-book (create-sell-book {:type :sell :quantity 2 :price 12 :book.order/id 107} {:type :sell :quantity 8 :price 13 :book.order/id 108}) + order {:type :buy :quantity 10 :price 13 :book.order/id 109} + [trades new-book] (match-order-in-book sell-book order)] + new-book => empty? + trades => [#:book.order{:quantity 2 :price 12 :book-id 107 :order-id 109 :type :sell} + #:book.order{:quantity 8 :price 13 :book-id 108 :order-id 109 :type :sell}])) + (fact "Simplest case V." + (let [sell-book (create-sell-book {:type :sell :quantity 2 :price 12 :book.order/id 113} {:type :sell :quantity 8 :price 13 :book.order/id 114}) + order {:type :buy :quantity 9 :price 13 :book.order/id 115} + [trades new-book] (match-order-in-book sell-book order)] + new-book => (create-sell-book {:type :sell :quantity 1 :price 13 :book.order/id 114}) + trades => [#:book.order{:quantity 2 :price 12 :book-id 113 :order-id 115 :type :sell} + #:book.order{:quantity 7 :price 13 :book-id 114 :order-id 115 :type :sell}])) + (fact "Simplest case VI." + (let [sell-book (create-sell-book {:type :sell :quantity 2 :price 12 :book.order/id 116} {:type :sell :quantity 8 :price 13 :book.order/id 117}) + order {:type :buy :quantity 21 :price 13 :book.order/id 118} + [trades new-book leftover-order] (match-order-in-book sell-book order)] + new-book => empty? + trades => [#:book.order{:quantity 2 :price 12 :book-id 116 :order-id 118 :type :sell} + #:book.order{:quantity 8 :price 13 :book-id 117 :order-id 118 :type :sell}] + leftover-order => {:type :buy :quantity 11 :price 13 :book.order/id 118})) + (fact "Simplest case VII." + (let [sell-book (create-sell-book {:type :sell :quantity 2 :price 12 :book.order/id 116} {:type :sell :quantity 8 :price 13 :book.order/id 117}) + order {:type :buy :quantity 21 :price 10 :book.order/id 118} + [trades new-book leftover-order] (match-order-in-book sell-book order)] + new-book => sell-book + trades => empty? + leftover-order => order))) + + +(facts "matching sell order in buy book" + (fact "Simplest case I." + (let [buy-book (create-buy-book {:type :buy :quantity 10 :price 2 :book.order/id 101}) + order {:type :sell :quantity 10 :price 2 :book.order/id 102} + [trades new-book] (match-order-in-book buy-book order)] + new-book => empty? + trades => [#:book.order{:quantity 10 :price 2 :book-id 101 :order-id 102 :type :buy}])) + + (fact "Simplest case II." + (let [buy-book (create-buy-book {:type :buy :quantity 10 :price 4 :book.order/id 103}) + order {:type :sell :quantity 10 :price 3 :book.order/id 104} + [trades new-book] (match-order-in-book buy-book order)] + new-book => [] + trades => [#:book.order{:quantity 10 :price 4 :book-id 103 :order-id 104 :type :buy}])) + + (fact "Simplest case III." + (let [buy-book (create-buy-book {:type :buy :quantity 10 :price 2 :book.order/id 105}) + order {:type :sell :quantity 8 :price 2 :book.order/id 106} + [trades new-book] (match-order-in-book buy-book order)] + new-book => [{:type :buy :quantity 2 :price 2 :book.order/id 105}] + trades => [#:book.order{:quantity 8 :price 2 :book-id 105 :order-id 106 :type :buy}])) + + (fact "Simplest case IV." + (let [buy-book (create-buy-book {:type :buy :quantity 2 :price 14 :book.order/id 107} + {:type :buy :quantity 8 :price 13 :book.order/id 108}) + order {:type :sell :quantity 10 :price 13 :book.order/id 109} + [trades new-book] (match-order-in-book buy-book order)] + new-book => [] + trades => [#:book.order{:quantity 2 :price 14 :book-id 107 :order-id 109 :type :buy} + #:book.order{:quantity 8 :price 13 :book-id 108 :order-id 109 :type :buy}])) + + (fact "Simplest case V." + (let [buy-book (create-buy-book {:type :buy :quantity 2 :price 14 :book.order/id 113} + {:type :buy :quantity 8 :price 13 :book.order/id 114}) + order {:type :sell :quantity 9 :price 13 :book.order/id 115} + [trades new-book] (match-order-in-book buy-book order)] + new-book => (create-buy-book {:type :buy :quantity 1 :price 13 :book.order/id 114}) + trades => [#:book.order{:quantity 2 :price 14 :book-id 113 :order-id 115 :type :buy} + #:book.order{:quantity 7 :price 13 :book-id 114 :order-id 115 :type :buy}])) + + (fact "Simplest case VI." + (let [sell-book (create-buy-book {:type :buy :quantity 2 :price 14 :book.order/id 116} + {:type :buy :quantity 8 :price 13 :book.order/id 117}) + order {:type :sell :quantity 21 :price 13 :book.order/id 118} + [trades new-book leftover-order] (match-order-in-book sell-book order)] + new-book => empty? + trades => [#:book.order{:quantity 2 :price 14 :book-id 116 :order-id 118 :type :buy} + #:book.order{:quantity 8 :price 13 :book-id 117 :order-id 118 :type :buy}] + leftover-order => {:type :sell :quantity 11 :price 13 :book.order/id 118}))) + +(facts "We should write tests for each bug found" + (fact "book type should always be book.PersistentSortedList" + (let [book (create-buy-book) + order {:type :sell :quantity 21 :price 13 :book.order/id 118} + [_ new-book _] (match-order-in-book book order)] + (count new-book) => 0 + (type new-book) => skirmish.PersistentSortedList))) \ No newline at end of file diff --git a/test/skirmish/match_test.clj b/test/skirmish/match_test.clj deleted file mode 100644 index 8904ae2..0000000 --- a/test/skirmish/match_test.clj +++ /dev/null @@ -1,124 +0,0 @@ -(ns skirmish.match-test - (:require [midje.sweet :refer :all] - [skirmish.match :refer :all])) - -(def PersistentSortedList? (comp #(= % skirmish.PersistentSortedList) type)) - - -(facts "matching buy order in sell book" - (fact "Simplest case I." - (let [sell-book (create-sell-book {:type :sell :quantity 10 :price 2 :book.order/id 101}) - order {:type :buy :quantity 10 :price 2 :book.order/id 102} - [trades new-book] (match-order-in-book sell-book order)] - new-book => empty? - trades => [#:book.order{:quantity 10 :price 2 :book-id 101 :order-id 102 :type :sell}])) - - (fact "Simplest case II." - (let [sell-book (create-sell-book {:type :sell :quantity 10 :price 2 :book.order/id 103}) - order {:type :buy :quantity 10 :price 3 :book.order/id 104} - [trades new-book] (match-order-in-book sell-book order)] - new-book => empty? - trades => [#:book.order{:quantity 10 :price 2 :book-id 103 :order-id 104 :type :sell}])) - (fact "Simplest case III." - (let [sell-book (create-sell-book {:type :sell :quantity 10 :price 2 :book.order/id 105}) - order {:type :buy :quantity 8 :price 2 :book.order/id 106} - [trades new-book] (match-order-in-book sell-book order)] - new-book => [{:type :sell :quantity 2 :price 2 :book.order/id 105}] - trades => [#:book.order{:quantity 8 :price 2 :book-id 105 :order-id 106 :type :sell}])) - (fact "Simplest case IV." - (let [sell-book (create-sell-book {:type :sell :quantity 2 :price 12 :book.order/id 107} {:type :sell :quantity 8 :price 13 :book.order/id 108}) - order {:type :buy :quantity 10 :price 13 :book.order/id 109} - [trades new-book] (match-order-in-book sell-book order)] - new-book => empty? - trades => [#:book.order{:quantity 2 :price 12 :book-id 107 :order-id 109 :type :sell} - #:book.order{:quantity 8 :price 13 :book-id 108 :order-id 109 :type :sell}])) - (fact "Simplest case V." - (let [sell-book (create-sell-book {:type :sell :quantity 2 :price 12 :book.order/id 113} {:type :sell :quantity 8 :price 13 :book.order/id 114}) - order {:type :buy :quantity 9 :price 13 :book.order/id 115} - [trades new-book] (match-order-in-book sell-book order)] - new-book => (create-sell-book {:type :sell :quantity 1 :price 13 :book.order/id 114}) - trades => [#:book.order{:quantity 2 :price 12 :book-id 113 :order-id 115 :type :sell} - #:book.order{:quantity 7 :price 13 :book-id 114 :order-id 115 :type :sell}])) - (fact "Simplest case VI." - (let [sell-book (create-sell-book {:type :sell :quantity 2 :price 12 :book.order/id 116} {:type :sell :quantity 8 :price 13 :book.order/id 117}) - order {:type :buy :quantity 21 :price 13 :book.order/id 118} - [trades new-book leftover-order] (match-order-in-book sell-book order)] - new-book => empty? - trades => [#:book.order{:quantity 2 :price 12 :book-id 116 :order-id 118 :type :sell} - #:book.order{:quantity 8 :price 13 :book-id 117 :order-id 118 :type :sell}] - leftover-order => {:type :buy :quantity 11 :price 13 :book.order/id 118})) - (fact "Simplest case VII." - (let [sell-book (create-sell-book {:type :sell :quantity 2 :price 12 :book.order/id 116} {:type :sell :quantity 8 :price 13 :book.order/id 117}) - order {:type :buy :quantity 21 :price 10 :book.order/id 118} - [trades new-book leftover-order] (match-order-in-book sell-book order)] - new-book => sell-book - trades => empty? - leftover-order => order))) - - -(facts "matching sell order in buy book" - (fact "Simplest case I." - (let [buy-book (create-buy-book {:type :buy :quantity 10 :price 2 :book.order/id 101}) - order {:type :sell :quantity 10 :price 2 :book.order/id 102} - [trades new-book] (match-order-in-book buy-book order)] - new-book => empty? - trades => [#:book.order{:quantity 10 :price 2 :book-id 101 :order-id 102 :type :buy}])) - - (fact "Simplest case II." - (let [buy-book (create-buy-book {:type :buy :quantity 10 :price 4 :book.order/id 103}) - order {:type :sell :quantity 10 :price 3 :book.order/id 104} - [trades new-book] (match-order-in-book buy-book order)] - new-book => [] - trades => [#:book.order{:quantity 10 :price 4 :book-id 103 :order-id 104 :type :buy}])) - - (fact "Simplest case III." - (let [buy-book (create-buy-book {:type :buy :quantity 10 :price 2 :book.order/id 105}) - order {:type :sell :quantity 8 :price 2 :book.order/id 106} - [trades new-book] (match-order-in-book buy-book order)] - new-book => [{:type :buy :quantity 2 :price 2 :book.order/id 105}] - trades => [#:book.order{:quantity 8 :price 2 :book-id 105 :order-id 106 :type :buy}])) - - (fact "Simplest case IV." - (let [buy-book (create-buy-book {:type :buy :quantity 2 :price 14 :book.order/id 107} - {:type :buy :quantity 8 :price 13 :book.order/id 108}) - order {:type :sell :quantity 10 :price 13 :book.order/id 109} - [trades new-book] (match-order-in-book buy-book order)] - new-book => [] - trades => [#:book.order{:quantity 2 :price 14 :book-id 107 :order-id 109 :type :buy} - #:book.order{:quantity 8 :price 13 :book-id 108 :order-id 109 :type :buy}])) - - (fact "Simplest case V." - (let [buy-book (create-buy-book {:type :buy :quantity 2 :price 14 :book.order/id 113} - {:type :buy :quantity 8 :price 13 :book.order/id 114}) - order {:type :sell :quantity 9 :price 13 :book.order/id 115} - [trades new-book] (match-order-in-book buy-book order)] - new-book => (create-buy-book {:type :buy :quantity 1 :price 13 :book.order/id 114}) - trades => [#:book.order{:quantity 2 :price 14 :book-id 113 :order-id 115 :type :buy} - #:book.order{:quantity 7 :price 13 :book-id 114 :order-id 115 :type :buy}])) - - (fact "Simplest case VI." - (let [sell-book (create-buy-book {:type :buy :quantity 2 :price 14 :book.order/id 116} - {:type :buy :quantity 8 :price 13 :book.order/id 117}) - order {:type :sell :quantity 21 :price 13 :book.order/id 118} - [trades new-book leftover-order] (match-order-in-book sell-book order)] - new-book => empty? - trades => [#:book.order{:quantity 2 :price 14 :book-id 116 :order-id 118 :type :buy} - #:book.order{:quantity 8 :price 13 :book-id 117 :order-id 118 :type :buy}] - leftover-order => {:type :sell :quantity 11 :price 13 :book.order/id 118}))) - -(facts "We should write tests for each bug found" - (fact "book type should always be book.PersistentSortedList" - (let [book (create-buy-book) - order {:type :sell :quantity 21 :price 13 :book.order/id 118} - [_ new-book _] (match-order-in-book book order)] - (count new-book) => 0 - (type new-book) => skirmish.PersistentSortedList))) - ;(fact "should not fuck up" - ; (let [order-1 {:type :buy :quantity 4 :price 4 :book.order/id 5} - ; order-2 {:type :sell :quantity 2 :price 3 :book.order/id 6} - ; book (create-buy-book order-1) - ; expt-trade #:book.order{} - ; [trades new-book new-order] (match-order-in-book book order-2)] - ; trades => 1 - ; new-order => nil? - ; new-book => {}))) -- 2.55.0 From 0ccbb6bc1a289e81b9d5c39b04ad7852005bdf24 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Wed, 11 Jul 2018 19:41:01 -0400 Subject: [PATCH 2/5] initial commit --- .gitignore | 1 + resources/config.edn | 1 + src/skirmish/core.clj | 25 +++++-- src/skirmish/exchange/core.clj | 17 +++++ src/skirmish/exchange/exchange.clj | 2 +- src/skirmish/kafka_exchange.clj | 41 ------------ src/skirmish/middleware/core.clj | 90 ++++++++++++++++++++++++++ src/skirmish/rest/core.clj | 41 +++++++++--- src/skirmish/rest/exchange.clj | 23 ++----- test/skirmish/middleware/test_core.clj | 4 ++ 10 files changed, 171 insertions(+), 74 deletions(-) create mode 100644 src/skirmish/exchange/core.clj delete mode 100644 src/skirmish/kafka_exchange.clj create mode 100644 src/skirmish/middleware/core.clj create mode 100644 test/skirmish/middleware/test_core.clj diff --git a/.gitignore b/.gitignore index 1511d86..dae6af5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ pom.xml.asc book.iml skirmish.iml full-stack +fixtures diff --git a/resources/config.edn b/resources/config.edn index 9ecd30b..296a801 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -1,4 +1,5 @@ { + :middleware :kafka :kafka {:bootstrap.servers #or [#env KAFKA_BOOTSTRAP_SERVERS "localhost:9092"] :topics-prefix "skirmish-01-"} diff --git a/src/skirmish/core.clj b/src/skirmish/core.clj index 9b4a57c..b479c9e 100644 --- a/src/skirmish/core.clj +++ b/src/skirmish/core.clj @@ -2,18 +2,29 @@ (:require [skirmish.cli.core] [skirmish.rest.core] [skirmish.cli.benchmark] - [aero.core :refer (read-config)]) + [skirmish.exchange.core] + [aero.core :refer (read-config)] + [clojure.core.async :as async]) (:gen-class)) (def apps {:api-server skirmish.rest.core/api-main - :exchange-core skirmish.rest.core/api-main ;; should be skirmish.exchange.core/api-main + :exchange-core skirmish.exchange.core/-main ;; should be skirmish.exchange.core/api-main + :bookkeeper skirmish.rest.core/bookeeper-main :benchmark skirmish.cli.benchmark/-main}) +(defn run-all-targets [target-fns config] + (let [threads (map #(async/thread (% config)) target-fns)] + (async/ "config.edn" clojure.java.io/resource read-config) - target (-> argv first keyword) - target-fn (get apps target)] - (if target-fn - (apply target-fn [config]) - (println "target" target "not found")))) \ No newline at end of file + xf (comp keyword? apps) + target-fns (sequence xf (rest argv))] + (if (seq target-fns) + (run-all-targets target-fns config) + (do + (println "target" target-fns "not found") + (println "all known targets are...") + (println (keys apps)))))) \ No newline at end of file diff --git a/src/skirmish/exchange/core.clj b/src/skirmish/exchange/core.clj new file mode 100644 index 0000000..20244cf --- /dev/null +++ b/src/skirmish/exchange/core.clj @@ -0,0 +1,17 @@ +(ns skirmish.exchange.core + (:require [skirmish.middleware.core :refer [create-exchange-apis-for]] + [skirmish.exchange.exchange :refer [create-exchange-for-security empty-book]] + [clojure.core.async :as async] + [kinsky.async] + [kinsky.client])) + +(defn create-exchange-for [exchange-api] + (let [initial-book empty-book + x (:new-order-publisher exchange-api)] + (create-exchange-for-security initial-book x))) + +(defn -main [config] + (let [exchange-apis (create-exchange-apis-for config)] + (doseq [{sec-id :sec-id} (:securities exchange-apis)] + (create-exchange-for (get exchange-apis sec-id))))) + ; ;api-routes (create-routes exchange-apis kafka-producer)]));; \ No newline at end of file diff --git a/src/skirmish/exchange/exchange.clj b/src/skirmish/exchange/exchange.clj index 72c1450..f99934a 100644 --- a/src/skirmish/exchange/exchange.clj +++ b/src/skirmish/exchange/exchange.clj @@ -79,7 +79,7 @@ ;; bellow are the really interesting functions. -(defn create-exchange-for-security [initial-book incoming-order-channel] +(defn create-exchange-for-security [initial-book incoming-order-channel trades-channel outcoming-orders-channel] ;; output - trades channel, pub channel ;; - orders channel, pub channel (let [trades-channel (chan) diff --git a/src/skirmish/kafka_exchange.clj b/src/skirmish/kafka_exchange.clj deleted file mode 100644 index f3bac69..0000000 --- a/src/skirmish/kafka_exchange.clj +++ /dev/null @@ -1,41 +0,0 @@ -(ns skirmish.kafka-exchange - (:require [kafka-streams-clojure.api :as api]) - (:import [org.apache.kafka.clients.producer KafkaProducer ProducerRecord] - [org.apache.kafka.streams StreamsConfig KafkaStreams] - [org.apache.kafka.streams.kstream KStreamBuilder])) - - -(defn -main [] - (def xform (comp (filter (fn [[k v]] (string? v))) - (map (fn [[k v]] [v k])) - (filter (fn [[k v]] (= "foo" v))))) - - (def builder (KStreamBuilder.)) - - (def kstream (-> builder - (.stream (into-array String ["tset"])) - (api/transduce-kstream xform) - (.to "test"))) - - (def kafka-streams - (KafkaStreams. builder - (StreamsConfig. {StreamsConfig/APPLICATION_ID_CONFIG "test-app-id" - StreamsConfig/BOOTSTRAP_SERVERS_CONFIG "localhost:9092" - StreamsConfig/KEY_SERDE_CLASS_CONFIG org.apache.kafka.common.serialization.Serdes$StringSerde - StreamsConfig/VALUE_SERDE_CLASS_CONFIG org.apache.kafka.common.serialization.Serdes$StringSerde}))) - (.start kafka-streams) - - (def producer (KafkaProducer. {"bootstrap.servers" "localhost:9092" - "acks" "all" - "retries" "0" - "key.serializer" "org.apache.kafka.common.serialization.StringSerializer" - "value.serializer" "org.apache.kafka.common.serialization.StringSerializer"})) - - @(.send producer (ProducerRecord. "tset" "foo" "bar")) - ;; Observe message come across topic "test" via kafka-console-consumer - - @(.send producer (ProducerRecord. "tset" "baz" "quux")) - ;; Observe message does not come across topic "test" via kafka-console-consumer - - (.close producer) - (.close kafka-streams)) \ No newline at end of file diff --git a/src/skirmish/middleware/core.clj b/src/skirmish/middleware/core.clj new file mode 100644 index 0000000..7b03f9a --- /dev/null +++ b/src/skirmish/middleware/core.clj @@ -0,0 +1,90 @@ +(ns skirmish.middleware.core + (:require [clojure.core.async :as async] + [skirmish.exchange.exchange :refer [empty-book create-exchange-for-security]] ;; remove this import. + [kinsky.async] + [kinsky.client])) + + +(defn set-kafka-as-middleware-in-exchange-api [sec-id api kafka-output-channel] + (let [str-sec-id (str sec-id) + xf (map #(hash-map :topic str-sec-id :key (:book.order/id %) :value %)) + from (async/chan 1 xf)] + (async/pipe from kafka-output-channel) + (assoc api :incoming-order-channel from))) + + +(defn create-async-kafka-producer [config] + (kinsky.async/producer (:kafka config) + (kinsky.client/edn-serializer) + (kinsky.client/edn-serializer))) + + +(defn set-middleware [exchange-apis config] + (if (= (:middleware config) :kafka) + (let [kafka-output-channel (create-async-kafka-producer config)] + (into {} (map (fn [[k v]] [k (set-kafka-as-middleware-in-exchange-api k v kafka-output-channel)]) exchange-apis))) + exchange-apis)) + +;(defn create-exchange-api-for [sec-id] +; (let [initial-book empty-book +; incoming-order-channel (async/chan) +; [trades-publisher new-book-order-publisher] (create-exchange-for-security initial-book incoming-order-channel)] +; {:sec-id sec-id +; :new-order-publisher nil ;; anything published here will be processed as +; :new-order-register nil +; :incoming-order-channel incoming-order-channel +; :trades-publisher-bis nil +; :trades-register nil +; :orders-publisher nil +; :orders-register nil +; +; :trades-publisher trades-publisher +; :new-book-order-publisher new-book-order-publisher})) + +(def chan-based-exchange-apis (atom {})) + +(defn create-new-chan-based-exchange-api-for [sec-id] + ;[(pub trades-channel :book.order/type) (pub outcoming-orders-channel :type)]) + (let [new-order (async/chan) + trades (async/chan) + orders (async/chan)] + {:sec-id sec-id + :new-order-publisher new-order ;; anything published here will be processed as + :new-order-register new-order + :trades-publisher trades + :trades-register (async/pub trades :book.order/type) + :orders-publisher orders + :orders-register (async/pub orders :type)})) + +(defn chan-based-exchange-api-for-swapper [sec-id exchange-api] + (if (contains? exchange-api sec-id) + exchange-api + (assoc exchange-api sec-id (create-new-chan-based-exchange-api-for sec-id)))) + +(defn create-chan-based-exchange-api-for [sec-id] + (let [swapper (partial chan-based-exchange-api-for-swapper sec-id) + exchange-apis (swap! chan-based-exchange-apis swapper)] + (get exchange-apis sec-id))) + +(defn create-kafka-based-exchange-api-for [sec-id] + (let [new-order (async/chan) + trades (async/chan) + orders (async/chan)] + {:sec-id sec-id + :new-order-publisher new-order ;; anything published here will be processed as + :new-order-register new-order + :trades-publisher-bis trades + :trades-register trades + :orders-publisher orders + :orders-register orders})) + +(def api-creators {:kafka create-kafka-based-exchange-api-for}) + +(defn create-exchange-apis-for [config] + ;; todo: spec this function + (let [security-definitions (map :sec-id (:securities config)) + create-exchange-api-for (:get api-creators (:middleware config) create-chan-based-exchange-api-for) + f (fn [sec] [(:sec-id sec) (create-exchange-api-for (:sec-id sec))]) + ret (into {} (map f security-definitions))] + (set-middleware ret config))) +;; \ No newline at end of file diff --git a/src/skirmish/rest/core.clj b/src/skirmish/rest/core.clj index 5ed50b2..e489dab 100644 --- a/src/skirmish/rest/core.clj +++ b/src/skirmish/rest/core.clj @@ -1,17 +1,42 @@ (ns skirmish.rest.core (:require [org.httpkit.server :refer [run-server]] [compojure.handler :refer [api]] + [clojure.core.async :as async] [kinsky.client] - [skirmish.rest.exchange :refer [create-exchanges-api-for - create-routes]])) + [kinsky.async] + [skirmish.middleware.core :refer [create-exchange-apis-for]] + [skirmish.rest.exchange :refer [create-routes]])) + + +(defn set-kafka-as-middleware-in-exchange-api [sec-id api kafka-output-channel] + (let [str-sec-id (str sec-id) + xf (map #(hash-map :topic str-sec-id :key (:book.order/id %) :value %)) + from (async/chan 1 xf)] + (async/pipe from kafka-output-channel) + (assoc api :incoming-order-channel from))) + + +(defn create-async-kafka-producer [config] + (kinsky.async/producer (:kafka config) + (kinsky.client/edn-serializer) + (kinsky.client/edn-serializer))) + + +(defn set-middleware [exchange-apis config] + (if (= (:middleware config) :kafka) + (let [kafka-output-channel (create-async-kafka-producer config)] + (into {} (map (fn [[k v]] [k (set-kafka-as-middleware-in-exchange-api k v kafka-output-channel)]) exchange-apis))) + exchange-apis)) + (defn api-main [config] (let [webserver-config (:webserver config) - exchange-apis (create-exchanges-api-for (:securities config)) - kafka-producer (kinsky.client/producer {:bootstrap.servers (get-in config [:kafka :bootstrap.servers])} - (kinsky.client/keyword-serializer) - (kinsky.client/edn-serializer)) - api-routes (create-routes exchange-apis kafka-producer)] + exchange-apis (create-exchange-apis-for config) + api-routes (create-routes (set-middleware exchange-apis config))] (println "Running server with bellow config") (println webserver-config) - (run-server (api api-routes) webserver-config))) \ No newline at end of file + (run-server (api api-routes) webserver-config))) + + +(defn bookeeper-main [config] + (println "not yet implemented")) \ No newline at end of file diff --git a/src/skirmish/rest/exchange.clj b/src/skirmish/rest/exchange.clj index e35d67b..28931bb 100644 --- a/src/skirmish/rest/exchange.clj +++ b/src/skirmish/rest/exchange.clj @@ -30,20 +30,6 @@ ;; -(defn create-exchange-api-for [sec-id] - (let [initial-book empty-book - incoming-order-channel (chan) - [trades-publisher new-book-order-publisher] (create-exchange-for-security initial-book incoming-order-channel)] - {:sec-id sec-id - :incoming-order-channel incoming-order-channel - :trades-publisher trades-publisher - :new-book-order-publisher new-book-order-publisher})) - -(defn create-exchanges-api-for [security-definitions] - (let [f (fn [sec] [(:sec-id sec) (create-exchange-api-for (:sec-id sec))])] - (into {} (map f security-definitions)))) - - (defn get-new-order-channel [exchange-apis sec-id] (let [exchange (get exchange-apis sec-id) new-book-order-channel (:new-book-order-publisher exchange) @@ -97,7 +83,7 @@ (throw+ {:error-type :4XX :status 404 :body {:error "Security not available."}}))) -(defn create-order [exchange-apis kafka-producer] +(defn create-order [exchange-apis] (fn [{{sec-id-str :sec-id} :route-params body :body :as request}] (try+ (let [asked-order (get-body body) @@ -117,25 +103,28 @@ (send! ws-chan (build-response-body request e)) (recur)))) + (defn listen-to-orders [exchange-apis] (fn [{{sec-id-str :sec-id} :route-params :as request}] (with-channel request ws-chan (let [order-channel (get-new-order-channel exchange-apis (keyword sec-id-str))] (chan-to-websocket request order-channel ws-chan))))) + (defn listen-to-trades [exchange-apis] (fn [{{sec-id-str :sec-id} :route-params :as request}] (with-channel request ws-chan (let [order-channel (get-new-trades-channel exchange-apis (keyword sec-id-str))] (chan-to-websocket request order-channel ws-chan))))) -(defn create-routes [exchange-apis kafka-producer] + +(defn create-routes [exchange-apis] (routes (context "/api/1" [] (GET "/" [] api-description) (GET "/securities" [] (securities exchange-apis)) (GET "/securities/:sec-id" [] (get-security exchange-apis)) - (POST "/securities/:sec-id" [] (create-order exchange-apis kafka-producer)) + (POST "/securities/:sec-id" [] (create-order exchange-apis)) (GET "/securities/:sec-id/trades" [] (listen-to-trades exchange-apis)) (GET "/securities/:sec-id/orders" [] (listen-to-orders exchange-apis))) (not-found "not found"))) diff --git a/test/skirmish/middleware/test_core.clj b/test/skirmish/middleware/test_core.clj new file mode 100644 index 0000000..d70557b --- /dev/null +++ b/test/skirmish/middleware/test_core.clj @@ -0,0 +1,4 @@ +(ns skirmish.middleware.test-core + (:require [midje.sweet :refer :all] + [skirmish.middleware.core :refer :all])) + -- 2.55.0 From f22466536701fb2a2871eafecac1cc8be80116e4 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Sat, 14 Jul 2018 20:22:00 -0400 Subject: [PATCH 3/5] Implemented middleware in a somewhat stateless fashion --- README.md | 33 ++++++----- resources/config.edn | 13 ++++- src/skirmish/cli/benchmark.clj | 2 +- src/skirmish/core.clj | 35 +++++++---- src/skirmish/exchange/core.clj | 23 +++++--- src/skirmish/exchange/exchange.clj | 26 ++++----- src/skirmish/middleware/core.clj | 81 +++----------------------- src/skirmish/rest/core.clj | 43 ++++---------- src/skirmish/rest/exchange.clj | 14 +++-- src/skirmish/util.clj | 4 ++ test/skirmish/middleware/test_core.clj | 9 +++ 11 files changed, 124 insertions(+), 159 deletions(-) diff --git a/README.md b/README.md index 84d484d..83d8c0f 100644 --- a/README.md +++ b/README.md @@ -6,29 +6,36 @@ A stock exchange simulator. The scope of this project is not yet defined but it allow end user(s?) to exchange securities via the common exchange rules. -## Installation -Okay, this should be easy but it is hard... +## Getting Started + +### Development + +In development, you need nothing else than [clojure](https://clojure.org/) +and [leiningen](https://leiningen.org/). The easiest way to install clojure is +to make sure you have java (8 and above) and install leiningen. + +### Production + +You will need plenty of things, the majority of them are yet to define. +But, a good kafka deployment will be required. Probably a database, +hopefully datomic. ### Configuration All configuration are set in the `resources/config.edn` file. -Configuration uses [aero](https://github.com/juxt/aero). +Configuration uses [aero](https://github.com/juxt/aero). The configuration +file should be self documented. ### Technologies #### Server -* clojure -* kafka +* [clojure](https://clojure.org/) for programming language +* [kafka](http://kafka.apache.org/) for message broker/database #### In the browser -* https://aftertheflood.com/projects/sparks/ -* http://metricsgraphicsjs.org/ - -## Usage - -FIXME: explanation - - $ java -jar book-0.1.0-standalone.jar [args] +* [clojure script](https://clojurescript.org/) for programming language +* [sparks](https://aftertheflood.com/projects/sparks/) for trivial charts +* [metricsgraphicsjs](http://metricsgraphicsjs.org/) for complex charts diff --git a/resources/config.edn b/resources/config.edn index 296a801..bf3716d 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -1,11 +1,18 @@ { + ;; :middleware :kafka - :kafka + :kafka ;; all the kafka configuration {:bootstrap.servers #or [#env KAFKA_BOOTSTRAP_SERVERS "localhost:9092"] :topics-prefix "skirmish-01-"} - :webserver ;; possible config from http-kit's run-server - {:port #long #or [#env PORT 8080] + :api-server ;; possible config from http-kit's run-server that will affect the api-server + {:port #long #or [#env API_SERVER_PORT 1602] + :ip "0.0.0.0" + :max-body 524288 ;; 512kb, small but enough + :max-ws 524288} + + :api-bookkeeper ;; possible config from http-kit's run-server that will affect the api-bookkeeper + {:port #long #or [#env API_SERVER_PORT 1603] :ip "0.0.0.0" :max-body 524288 ;; 512kb, small but enough :max-ws 524288} diff --git a/src/skirmish/cli/benchmark.clj b/src/skirmish/cli/benchmark.clj index a948b90..882e7b0 100644 --- a/src/skirmish/cli/benchmark.clj +++ b/src/skirmish/cli/benchmark.clj @@ -94,7 +94,7 @@ (println "status map: " (:status results)) (pp-durations (:durations results))) -(defn -main [config] +(defn -main [config exchange-apis] (let [benchmark-config (:benchmark config) requests (-> benchmark-config :number-of-requests-per-securities (make-requests (:securities config))) orders (create-orders-channel (:securities config)) diff --git a/src/skirmish/core.clj b/src/skirmish/core.clj index b479c9e..ef0d680 100644 --- a/src/skirmish/core.clj +++ b/src/skirmish/core.clj @@ -3,28 +3,43 @@ [skirmish.rest.core] [skirmish.cli.benchmark] [skirmish.exchange.core] + [skirmish.middleware.core :refer [create-exchange-apis-for]] [aero.core :refer (read-config)] [clojure.core.async :as async]) (:gen-class)) -(def apps {:api-server skirmish.rest.core/api-main - :exchange-core skirmish.exchange.core/-main ;; should be skirmish.exchange.core/api-main - :bookkeeper skirmish.rest.core/bookeeper-main - :benchmark skirmish.cli.benchmark/-main}) +(def targets ;; a target is a function that blocks the thread and takes two + ;; arguments: the config map and the "exchange-api" + {:api-server skirmish.rest.core/api-main + :exchange-core skirmish.exchange.core/-main ;; should be skirmish.exchange.core/api-main + :api-bookkeeper skirmish.rest.core/bookeeper-main}) + ;:benchmark skirmish.cli.benchmark/-main}) + +(defn run-all-targets-in-separate-threads [target-fns config exchange-apis] + (println "running multiple targets. This is okay in dev but horrible in production") + (println target-fns) + (let [threads (map #(async/thread (% config exchange-apis)) target-fns) + finished-threads (async/merge threads)];; need to lock here + (loop [finished-thread (async/ "config.edn" clojure.java.io/resource read-config) - xf (comp keyword? apps) - target-fns (sequence xf (rest argv))] + xf (comp (map keyword) (map targets) (filter some?)) + target-fns (sequence xf argv)] (if (seq target-fns) (run-all-targets target-fns config) (do + (println argv) (println "target" target-fns "not found") (println "all known targets are...") - (println (keys apps)))))) \ No newline at end of file + (println (keys targets)))))) \ No newline at end of file diff --git a/src/skirmish/exchange/core.clj b/src/skirmish/exchange/core.clj index 20244cf..0d3b770 100644 --- a/src/skirmish/exchange/core.clj +++ b/src/skirmish/exchange/core.clj @@ -1,17 +1,22 @@ (ns skirmish.exchange.core - (:require [skirmish.middleware.core :refer [create-exchange-apis-for]] + (:require [skirmish.util :refer [block-thread]] + [skirmish.middleware.core :refer [create-exchange-apis-for]] [skirmish.exchange.exchange :refer [create-exchange-for-security empty-book]] - [clojure.core.async :as async] [kinsky.async] [kinsky.client])) (defn create-exchange-for [exchange-api] (let [initial-book empty-book - x (:new-order-publisher exchange-api)] - (create-exchange-for-security initial-book x))) + new-order-publisher (:new-order-publisher exchange-api) + trades-submitter (:trades-submitter exchange-api) + orders-submitter (:orders-submitter exchange-api)] -(defn -main [config] - (let [exchange-apis (create-exchange-apis-for config)] - (doseq [{sec-id :sec-id} (:securities exchange-apis)] - (create-exchange-for (get exchange-apis sec-id))))) - ; ;api-routes (create-routes exchange-apis kafka-producer)]));; \ No newline at end of file + (create-exchange-for-security initial-book + new-order-publisher + trades-submitter + orders-submitter))) + +(defn -main [config exchange-apis] + (doseq [{sec-id :sec-id} (:securities config)] + (create-exchange-for (get exchange-apis sec-id))) + (block-thread)) \ No newline at end of file diff --git a/src/skirmish/exchange/exchange.clj b/src/skirmish/exchange/exchange.clj index f99934a..464fe01 100644 --- a/src/skirmish/exchange/exchange.clj +++ b/src/skirmish/exchange/exchange.clj @@ -79,22 +79,16 @@ ;; bellow are the really interesting functions. -(defn create-exchange-for-security [initial-book incoming-order-channel trades-channel outcoming-orders-channel] - ;; output - trades channel, pub channel - ;; - orders channel, pub channel - (let [trades-channel (chan) - outcoming-orders-channel (chan)] - (async/go - (loop [order (!! incoming-order-channel order) (build-response request order)) @@ -118,13 +118,19 @@ (chan-to-websocket request order-channel ws-chan))))) -(defn create-routes [exchange-apis] +(defn create-routes-for-exchange [exchange-apis] + (routes + (context "/api/1" [] + (POST "/securities/:sec-id" [] (create-order exchange-apis))) + (not-found "not found"))) + + +(defn create-routes-for-bookkeeper [exchange-apis] (routes (context "/api/1" [] (GET "/" [] api-description) (GET "/securities" [] (securities exchange-apis)) (GET "/securities/:sec-id" [] (get-security exchange-apis)) - (POST "/securities/:sec-id" [] (create-order exchange-apis)) (GET "/securities/:sec-id/trades" [] (listen-to-trades exchange-apis)) (GET "/securities/:sec-id/orders" [] (listen-to-orders exchange-apis))) (not-found "not found"))) diff --git a/src/skirmish/util.clj b/src/skirmish/util.clj index 18483f5..995a47c 100644 --- a/src/skirmish/util.clj +++ b/src/skirmish/util.clj @@ -1,4 +1,5 @@ (ns skirmish.util + (:require [clojure.core.async :as async]) (:import [skirmish.PersistentSortedList])) (defn sorted-list [& keys] @@ -7,3 +8,6 @@ (defn sorted-list-by [comparator & keys] (skirmish.PersistentSortedList/create comparator (seq keys))) +(defn block-thread [] + "Block the current thread until chuck norris gets weaker." + (async/ [:molly :pepper] + (vals exchanges) => #(every? map? %)))) \ No newline at end of file -- 2.55.0 From baf0d9840cf63cb444cd873194777cd5222fe08d Mon Sep 17 00:00:00 2001 From: didier amyot Date: Sun, 15 Jul 2018 18:24:52 -0400 Subject: [PATCH 4/5] somehow, things start to work togheter --- src/skirmish/rest/{marketdata.clj => bookkeeper.clj} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/skirmish/rest/{marketdata.clj => bookkeeper.clj} (100%) diff --git a/src/skirmish/rest/marketdata.clj b/src/skirmish/rest/bookkeeper.clj similarity index 100% rename from src/skirmish/rest/marketdata.clj rename to src/skirmish/rest/bookkeeper.clj -- 2.55.0 From 27b4fd572720858ed567c63a1d693d0e21a94beb Mon Sep 17 00:00:00 2001 From: didier amyot Date: Sun, 15 Jul 2018 18:25:30 -0400 Subject: [PATCH 5/5] somehow, things start to work togheter --- project.clj | 1 + resources/config.edn | 19 +++- src/skirmish/core.clj | 8 +- src/skirmish/exchange/exchange.clj | 43 +++++--- src/skirmish/middleware/core.clj | 2 +- src/skirmish/order.clj | 23 ++--- src/skirmish/rest/bookkeeper.clj | 119 ++++++++++++++++++++++- src/skirmish/rest/core.clj | 6 +- src/skirmish/rest/exchange.clj | 74 -------------- test/skirmish/exchange/exchange_test.clj | 15 ++- 10 files changed, 197 insertions(+), 113 deletions(-) diff --git a/project.clj b/project.clj index 7a6090a..23fbd81 100644 --- a/project.clj +++ b/project.clj @@ -19,6 +19,7 @@ [slingshot "0.12.2"] [spootnik/kinsky "0.1.22"] [org.clojars.fossi/kafka-streams-clojure "0.1.0-SNAPSHOT"] ;; JEAH! + [orchestra "2017.11.12-1"] [cheshire "5.8.0"]] :java-source-paths ["src/java"] :main ^:skip-aot skirmish.core diff --git a/resources/config.edn b/resources/config.edn index bf3716d..7ba8069 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -1,28 +1,37 @@ { - ;; + ;; Specify which middleware to use, it will default to :chan. :middleware :kafka - :kafka ;; all the kafka configuration + + ;; If set to true, orchestra will instrument all the spec'ed functions + :instrument-spec true + + ;; all the kafka configuration goes bellow. + :kafka {:bootstrap.servers #or [#env KAFKA_BOOTSTRAP_SERVERS "localhost:9092"] :topics-prefix "skirmish-01-"} - :api-server ;; possible config from http-kit's run-server that will affect the api-server + ;; possible config from http-kit's run-server that will affect the api-server + :api-server {:port #long #or [#env API_SERVER_PORT 1602] :ip "0.0.0.0" :max-body 524288 ;; 512kb, small but enough :max-ws 524288} - :api-bookkeeper ;; possible config from http-kit's run-server that will affect the api-bookkeeper + ;; possible config from http-kit's run-server that will affect the api-bookkeeper + :api-bookkeeper {:port #long #or [#env API_SERVER_PORT 1603] :ip "0.0.0.0" :max-body 524288 ;; 512kb, small but enough :max-ws 524288} - :securities ;; list of tradable securities + ;; list of tradable securities + :securities [{:sec-id :foo} {:sec-id :bar} {:sec-id :toto} {:sec-id :tata}] + ;; stupid parameters about benchmarking tools :benchmark { :number-of-requests-per-securities 30}} diff --git a/src/skirmish/core.clj b/src/skirmish/core.clj index ef0d680..d9f0889 100644 --- a/src/skirmish/core.clj +++ b/src/skirmish/core.clj @@ -5,6 +5,7 @@ [skirmish.exchange.core] [skirmish.middleware.core :refer [create-exchange-apis-for]] [aero.core :refer (read-config)] + [orchestra.spec.test :as st] [clojure.core.async :as async]) (:gen-class)) @@ -12,17 +13,16 @@ ;; arguments: the config map and the "exchange-api" {:api-server skirmish.rest.core/api-main :exchange-core skirmish.exchange.core/-main ;; should be skirmish.exchange.core/api-main - :api-bookkeeper skirmish.rest.core/bookeeper-main}) + :api-bookkeeper skirmish.rest.core/bookkeeper-main}) ;:benchmark skirmish.cli.benchmark/-main}) (defn run-all-targets-in-separate-threads [target-fns config exchange-apis] (println "running multiple targets. This is okay in dev but horrible in production") (println target-fns) (let [threads (map #(async/thread (% config exchange-apis)) target-fns) - finished-threads (async/merge threads)];; need to lock here + finished-threads (async/merge threads)] (loop [finished-thread (async/ "config.edn" clojure.java.io/resource read-config) xf (comp (map keyword) (map targets) (filter some?)) target-fns (sequence xf argv)] + (if (true? (:instrument-spec config)) + (st/instrument)) (if (seq target-fns) (run-all-targets target-fns config) (do diff --git a/src/skirmish/exchange/exchange.clj b/src/skirmish/exchange/exchange.clj index 464fe01..5b692bc 100644 --- a/src/skirmish/exchange/exchange.clj +++ b/src/skirmish/exchange/exchange.clj @@ -4,6 +4,7 @@ create-sell-book match-order-in-book]] [skirmish.util :refer :all] + [clojure.spec.alpha :as s] [clojure.core.async :refer [chan go >! expt-book-4))) (facts "get-processed-orders" - (fact "ss" + (fact "bug numero uno" (let [sell-book (create-buy-book {:type :buy :quantity 2 :price 14 :book.order/id 116} {:type :buy :quantity 8 :price 13 :book.order/id 117}) order {:type :sell :quantity 21 :price 13 :book.order/id 118} [trades new-book leftover-order] (match-order-in-book sell-book order) processed-order (get-processed-orders order leftover-order trades sell-book)] - processed-order => seq)));;; + processed-order => seq));;; + (fact "bug numero dos" + (let [order {:type :sell, :quantity 10, :price 2, :book.order/id 1} + leftover-order nil + trades [#:book.order{:type :buy, :quantity 10, :price 3, :order-id 1, :book-id 0}] + book {:sell (create-sell-book) + :buy (create-buy-book {:type :buy, :quantity 15, :price 3, :book.order/id 0})} + ret (get-processed-orders order leftover-order trades (:buy book))] + (count ret) => 3))) -- 2.55.0