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/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/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/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 9ecd30b..7ba8069 100644 --- a/resources/config.edn +++ b/resources/config.edn @@ -1,20 +1,37 @@ { + ;; Specify which middleware to use, it will default to :chan. + :middleware :kafka + + ;; 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-"} - :webserver ;; possible config from http-kit's run-server - {:port #long #or [#env PORT 8080] + ;; 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} - :securities ;; list of tradable securities + ;; 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} + + ;; 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/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/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..d9f0889 100644 --- a/src/skirmish/core.clj +++ b/src/skirmish/core.clj @@ -2,18 +2,46 @@ (:require [skirmish.cli.core] [skirmish.rest.core] [skirmish.cli.benchmark] - [skirmish.kafka.exchange-simulator] - [aero.core :refer (read-config)]) + [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)) -(def apps {:webserver skirmish.rest.core/-main - :benchmark skirmish.cli.benchmark/-main - :kafka-exchange-simulator skirmish.kafka.exchange-simulator/-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/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)] + (loop [finished-thread (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 (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 + (println argv) + (println "target" target-fns "not found") + (println "all known targets are...") + (println (keys targets)))))) \ 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..0d3b770 --- /dev/null +++ b/src/skirmish/exchange/core.clj @@ -0,0 +1,22 @@ +(ns skirmish.exchange.core + (:require [skirmish.util :refer [block-thread]] + [skirmish.middleware.core :refer [create-exchange-apis-for]] + [skirmish.exchange.exchange :refer [create-exchange-for-security empty-book]] + [kinsky.async] + [kinsky.client])) + +(defn create-exchange-for [exchange-api] + (let [initial-book empty-book + new-order-publisher (:new-order-publisher exchange-api) + trades-submitter (:trades-submitter exchange-api) + orders-submitter (:orders-submitter exchange-api)] + + (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.clj b/src/skirmish/exchange/exchange.clj similarity index 52% rename from src/skirmish/exchange.clj rename to src/skirmish/exchange/exchange.clj index 1bd8a02..5b692bc 100644 --- a/src/skirmish/exchange.clj +++ b/src/skirmish/exchange/exchange.clj @@ -1,9 +1,10 @@ -(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.spec.alpha :as s] [clojure.core.async :refer [chan go >! 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..ef3aff9 --- /dev/null +++ b/src/skirmish/middleware/core.clj @@ -0,0 +1,27 @@ +(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 create-chan-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-submitter new-order + :trades-publisher (async/pub trades :book.order/type) + :trades-submitter trades + :orders-publisher (async/pub orders :book.order/status) + :orders-submitter orders})) + +(def api-creators {}) + +(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 (create-exchange-api-for sec)]) + ret (into {} (map f security-definitions))] + ret)) \ No newline at end of file diff --git a/src/skirmish/order.clj b/src/skirmish/order.clj index a505369..78e61d7 100644 --- a/src/skirmish/order.clj +++ b/src/skirmish/order.clj @@ -7,18 +7,19 @@ ;; :type one of :sell :buy ;; ::id maybe an id -(s/def :book.order/type #{:buy :sell}) -(s/def :book.order/price pos-int?) -(s/def :book.order/quantity pos-int?) -(s/def :book.order/id pos-int?) -(s/def :book.order/status #{:inserted ;; maybe called acknklowdlge. - :partially-matched - :totally-matched - :inserted-into-book - :partially-fulfilled - :totally-fulfilled - :withdrawn}) ;; for instance the order has been canceled +(def order-status #{:received ;; maybe called acknowledged. + :partially-matched + :totally-matched + :inserted-into-book + :partially-fulfilled + :totally-fulfilled + :withdrawn}) ;; for instance the order has been canceled) +(s/def :book.order/type #{:buy :sell}) +(s/def :book.order/price nat-int?) +(s/def :book.order/quantity nat-int?) +(s/def :book.order/id nat-int?) +(s/def :book.order/status order-status) (s/def :book.order/status-dual-id :book.order/id) diff --git a/src/skirmish/rest/bookkeeper.clj b/src/skirmish/rest/bookkeeper.clj new file mode 100644 index 0000000..b708150 --- /dev/null +++ b/src/skirmish/rest/bookkeeper.clj @@ -0,0 +1,118 @@ +(ns skirmish.rest.bookkeeper + (:require [skirmish.exchange.exchange :refer [empty-book create-exchange-for-security]] + [org.httpkit.server :refer [run-server with-channel send! close on-close]] + [compojure.route :refer [not-found]] + [compojure.handler :refer [api]] + [compojure.core :refer :all] + [skirmish.order] ;; for spec side effect + [skirmish.rest.util :refer [build-response build-response-body]] + [clojure.spec.alpha :as s] + [kinsky.client] + [slingshot.slingshot :refer [try+ throw+]] + [clojure.core.async :refer [chan go-loop close! !! incoming-order-channel order) (build-response request order)) @@ -111,32 +54,9 @@ (catch [:error-type :5XX] e (build-response request e))))) -(defn chan-to-websocket [request input-chan ws-chan] - (on-close ws-chan (fn [status] (close! input-chan))) - (go-loop [] - (when-let [e ( 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))) diff --git a/test/skirmish/exchange/match_test.clj b/test/skirmish/exchange/match_test.clj new file mode 100644 index 0000000..8e7eb4b --- /dev/null +++ b/test/skirmish/exchange/match_test.clj @@ -0,0 +1,116 @@ +(ns skirmish.exchange.match-test + (:require [midje.sweet :refer :all] + [skirmish.exchange.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))) \ 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 => {}))) diff --git a/test/skirmish/middleware/test_core.clj b/test/skirmish/middleware/test_core.clj new file mode 100644 index 0000000..a484f48 --- /dev/null +++ b/test/skirmish/middleware/test_core.clj @@ -0,0 +1,13 @@ +(ns skirmish.middleware.test-core + (:require [midje.sweet :refer :all] + [skirmish.middleware.core :refer :all])) + +(defn all? [xf] + #(every? true? (sequence xf %))) + +(facts "we should have a async/chan base middleware for developing." + (fact "It should exchanges for each security in the config" + (let [config {:securities [{:sec-id :molly} {:sec-id :pepper}]} + exchanges (create-exchange-apis-for config)] + (keys exchanges) => [:molly :pepper] + (vals exchanges) => #(every? map? %)))) \ No newline at end of file