diff --git a/project.clj b/project.clj index c6b950c..9f97f0a 100644 --- a/project.clj +++ b/project.clj @@ -19,5 +19,6 @@ :plugins [[lein-midje "3.2.1"]] :profiles { :uberjar {:aot :all} - :dev {:dependencies [[midje "1.9.1"]]}}) + :dev {:jvm-opts ["-XX:-OmitStackTraceInFastThrow"] + :dependencies [[midje "1.9.1"]]}}) diff --git a/src/book/cli.clj b/src/book/cli.clj index 4a2147f..daa5384 100644 --- a/src/book/cli.clj +++ b/src/book/cli.clj @@ -1,7 +1,7 @@ (ns book.cli (:require [book.match :refer [sell?]] [book.util :refer :all] - [book.exchange :refer [empty-book process-orders create-exchange-for-security]] + [book.exchange :refer [empty-book create-exchange-for-security-2]] [book.order] ;; for spec side effect [slingshot.slingshot :refer [try+ throw+]] [clojure.edn :as edn] @@ -42,7 +42,7 @@ (defn -main [& args] (let [initial-book empty-book incoming-order-channel (chan) - [trades-channel new-book-order-channel] (create-exchange-for-security initial-book incoming-order-channel)] + [trades-channel new-book-order-channel] (create-exchange-for-security-2 initial-book incoming-order-channel)] (go (print-chan "trade > " trades-channel)) (go (print-chan "order > " new-book-order-channel)) (loop [order (read-order)] diff --git a/src/book/exchange.clj b/src/book/exchange.clj index 77dbdcb..0647d4c 100644 --- a/src/book/exchange.clj +++ b/src/book/exchange.clj @@ -4,37 +4,43 @@ create-sell-book match-order-in-book]] [book.util :refer :all] - [clojure.core.async :refer [chan go >!! !! ! pub]])) (def empty-book {:sell (create-sell-book) :buy (create-buy-book)}) -(defn process-abstract-sell [order reference-book same-type-book trades-channel new-book-order-channel] +(defn process-abstract [order reference-book same-type-book] (let [[trades new-reference-book leftover-order] (match-order-in-book reference-book order) - new-same-type-book (if leftover-order (conj same-type-book leftover-order) leftover-order)] - (when (seq trades) (doseq [trade trades] (>!! trades-channel trade))) - (when leftover-order (>!! new-book-order-channel leftover-order)) - [new-reference-book new-same-type-book])) + ;ff (println ">>=" same-type-book (type same-type-book));; + ;ff (println ">>=" leftover-order (type leftover-order)) + new-same-type-book (if leftover-order (conj same-type-book leftover-order) same-type-book)] + [new-reference-book new-same-type-book leftover-order trades])) -(defn process-sell-order [order {sell-book :sell buy-book :buy} trades-channel new-book-order-channel] - (let [[new-buy-book new-sell-book] (process-abstract-sell order buy-book sell-book trades-channel new-book-order-channel)] - {:sell new-sell-book :buy new-buy-book})) +(defn process-sell-order-2 [{sell-book :sell buy-book :buy} order] + (let [[new-buy-book new-sell-book new-order trades] (process-abstract order buy-book sell-book) + new-book {:sell new-sell-book :buy new-buy-book}] + [new-book new-order trades])) -(defn process-buy-order [order {sell-book :sell buy-book :buy} trades-channel new-book-order-channel] - (let [[new-sell-book new-buy-book] (process-abstract-sell order sell-book buy-book trades-channel new-book-order-channel)] - {:sell new-sell-book :buy new-buy-book})) +(defn process-buy-order-2 [{sell-book :sell buy-book :buy} order] + (let [[new-sell-book new-buy-book new-order trades] (process-abstract order sell-book buy-book) + new-book {:sell new-sell-book :buy new-buy-book}] + [new-book new-order trades])) -(defn process-orders [master-book orders-channel trades-channel new-book-order-channel] - (loop [order (! new-book-order-channel new-order)) + (doseq [trade new-trades] (>! trades-channel trade)) + (recur ( trades count dec) sell-book) +(defn- drop-in-sorted-list [n sorted-list] + (cond + (<= n 0) sorted-list + (<= (count sorted-list) n) (empty sorted-list) + :else (drop n sorted-list)));; + +(defn compute-new-book-after-trades [book trades] + (let [new-book (drop-in-sorted-list (-> trades count dec) book) possible-partial-trade (last trades)] (if (partial-trade? possible-partial-trade (first new-book)) (conj (rest new-book) (compute-partial-order (first new-book) possible-partial-trade)) @@ -93,20 +99,3 @@ new-buy-order (leftover-order order trades)] [trades new-book new-buy-order])) -;(defn place-buy-order [book order] -; (match-buy-order-in-sell-book (:sell book) order) -; (println :bingo)) ;; should return multiple "trade" and a new book -; -;(defn place-order [book order] -; (if (buy? order) -; (place-buy-order book order))) -; -; -;(defn -main -; [& args] -; (let [order (-> a+rgs first edn/read-string)] -; (if (not (s/valid? :book.order/order order)) -; (println "invalid order :( ") -; (println (place-order book order))))) -; -; diff --git a/src/book/rest/util.clj b/src/book/rest/util.clj index 51aeaed..6b7ab1b 100644 --- a/src/book/rest/util.clj +++ b/src/book/rest/util.clj @@ -36,8 +36,12 @@ 200) status)) +(defn build-response-body [request body] + (let [[_ serializer] (get-best-serializer request)] + (serializer body))) + (defn build-response [request {status :status body :body :as response}] (let [[content-type serializer] (get-best-serializer request)] - {:body (serializer response) + {:body (serializer (if (nil? body) response body)) :headers {"Content-Type" content-type} :status (get-status status request)})) \ No newline at end of file diff --git a/src/book/web.clj b/src/book/web.clj index 25aa472..9618d0d 100644 --- a/src/book/web.clj +++ b/src/book/web.clj @@ -1,14 +1,15 @@ (ns book.web (:require [book.exchange :refer [empty-book]] - [org.httpkit.server :refer [run-server]] + [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] [book.order] ;; for spec side effect - [book.rest.util :refer [build-response]] + [book.exchange :refer [create-exchange-for-security-2]] + [book.rest.util :refer [build-response build-response-body]] [clojure.spec.alpha :as s] [slingshot.slingshot :refer [try+ throw+]] - [cheshire.core :refer [generate-string]] + [clojure.core.async :refer [chan go go-loop close! !! sub]] [clojure.edn :as edn])) @@ -29,49 +30,114 @@ ;; -(defn securities [books] +(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-2 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 get-new-order-channel [exchange-apis sec-id] + (let [exchange (get exchange-apis sec-id) + new-book-order-channel (:new-book-order-publisher exchange) + ret (chan)] + (sub new-book-order-channel :sell ret) + (sub new-book-order-channel :buy ret) + ret)) + + +(defn get-new-trades-channel [exchange-apis sec-id] + (let [exchange (get exchange-apis sec-id) + new-trades-channel (:trades-publisher exchange) + ret (chan)] + (sub new-trades-channel :sell ret) + (sub new-trades-channel :buy ret) + ret)) + + +(defn securities [exchange-apis] (fn [request] - (build-response request {:securities (keys books)}))) - -(defn get-security [books] - (fn [{{sec-id-str :sec-id} :route-params body :body :as request}])) - -(defn validate-order [asked-order] - (if (not (s/valid? :book.order/order asked-order)) - (throw+ {:status :4XX :headers {"Content-Type" "application/json"} :body "foo"})) - (assoc asked-order :book.order/id (swap! order-id inc))) + (build-response request {:securities (keys exchange-apis)}))) -(defn get-order [body] +(defn get-security [exchange-apis] + (fn [{{sec-id-str :sec-id} :route-params body :body :as request}] + (build-response request {:security {:sec-id sec-id-str}}))) + + +(defn validate-order [asked-order sec-id] + (let [now (System/currentTimeMillis)] + (if (not (s/valid? :book.order/order asked-order)) + (throw+ {:error-type :4XX + :status 400 + :body {:error "Invalid order." + :spec (s/explain-data :book.order/order asked-order)}})) + (into asked-order {:book.order/id (swap! order-id inc) + :book.order/sec-id sec-id + :book.order/entry-timestamp now}))) + + +(defn get-body [body] (try+ (-> body clojure.java.io/reader slurp edn/read-string) (catch Exception e - (throw+ {:error-type :4XX :status 400 :body {:error "Invalid body format. Body must be edn format."}})))) + (throw+ {:error-type :4XX :status 400 :body {:error "Invalid body format. Body must be edn format."}})))) -(defn create-order [books] +(defn get-exchange-api [sec-id exchange-apis] + (if (contains? exchange-apis sec-id) + (get exchange-apis sec-id) + (throw+ {:error-type :4XX :status 404 :body {:error "Security not available."}}))) + + +(defn create-order [exchange-apis] (fn [{{sec-id-str :sec-id} :route-params body :body :as request}] (try+ - (let [asked-order (get-order body) - order (validate-order asked-order) - sec-id (keyword sec-id-str) - book (get books sec-id)] - (build-response request {:order order :book book})) + (let [asked-order (get-body body) + exchange-api (get-exchange-api (keyword sec-id-str) exchange-apis) + incoming-order-channel (:incoming-order-channel exchange-api) + order (validate-order asked-order (:sec-id exchange-api))] + (>!! incoming-order-channel order) + (build-response request order)) (catch [:error-type :4XX] e (build-response request e)) (catch [:error-type :5XX] e (build-response request e))))) -(defn create-routes [books] +(defn chan-to-websocket [request chan ws-chan] + (on-close ws-chan (fn [status] (close! chan))) + (go-loop [] + (when-let [e (!! !! incoming-order-channel {:type :sell :quantity 10 :price 2}) - ( {:type :sell :quantity 10 :price 2} - (>!! incoming-order-channel {:type :buy :quantity 20 :price 2}) - ( #:book.order{:book-id nil - :order-id nil - :price 2 - :quantity 10 - :type :sell} - ( {:type :buy :quantity 10 :price 2}))) + (fact "process-sell-order should work when book empty" + (let [book empty-book + order {:type :sell :quantity 10 :price 2 :book.order/id 1} + expt-new-sell-book [order] + [{new-sell-book :sell new-buy-book :buy} new-order new-trades] (process-sell-order-2 book order)] + new-sell-book => expt-new-sell-book + new-buy-book => empty? + new-order => order + new-trades => empty?)) + (fact "process-sell-order should work when there is a buy order that matches" + (let [sell-book (create-sell-book) + buy-book (create-buy-book {:type :buy :quantity 10 :price 3 :book.order/id 2}) + book {:sell sell-book :buy buy-book} + order {:type :sell :quantity 4 :price 2 :book.order/id 3} + expt-new-buy-book [{:price 3 :quantity 6 :type :buy :book.order/id 2}] + expt-new-trades [#:book.order{:book-id 2 :order-id 3 :price 3 :quantity 4 :type :buy}] + [{new-sell-book :sell new-buy-book :buy} new-order new-trades] (process-order-2 book order)] + new-sell-book => empty? + new-buy-book => expt-new-buy-book + new-order => nil? + new-trades => expt-new-trades)) + (fact "process-buy-order should work when book empty" + (let [book empty-book + order {:type :buy :quantity 10 :price 2 :book.order/id 1} + expt-new-sell-book [order] + [{new-sell-book :sell new-buy-book :buy} new-order new-trades] (process-order-2 book order)] + new-sell-book => empty? + new-buy-book => expt-new-sell-book + new-order => order + new-trades => empty?)) + (fact "process-buy-order should work when there is a buy order that matches" + (let [sell-book (create-sell-book {:type :sell :quantity 10 :price 3 :book.order/id 2}) + buy-book (create-buy-book) + book {:sell sell-book :buy buy-book} + order {:type :buy :quantity 4 :price 4 :book.order/id 3} + expt-new-sell-book [{:price 3 :quantity 6 :type :sell :book.order/id 2}] + expt-new-trades [#:book.order{:book-id 2 :order-id 3 :price 3 :quantity 4 :type :sell}] + [{new-sell-book :sell new-buy-book :buy} new-order new-trades] (process-order-2 book order)] + new-sell-book => expt-new-sell-book + new-order => nil? + new-trades => expt-new-trades)) + + (fact "process-order total dance" + (let [book empty-book + order-1 {:type :buy :quantity 10 :price 100 :book.order/id 7} + order-2 {:type :buy :quantity 15 :price 99 :book.order/id 8} + order-3 {:type :sell :quantity 17 :price 99 :book.order/id 9} + order-4 {:type :sell :quantity 1 :price 102 :book.order/id 10} + expt-book-4 {:buy [{:price 99 :quantity 8 :type :buy :book.order/id 8}] + :sell [{:price 102 :quantity 1 :type :sell :book.order/id 10}]} + [book-1 _ _] (process-order-2 book order-1) + [book-2 _ _] (process-order-2 book-1 order-2) + [book-3 _ _] (process-order-2 book-2 order-3) + [book-4 _ _] (process-order-2 book-3 order-4)] + book-4 => expt-book-4))) diff --git a/test/book/match_test.clj b/test/book/match_test.clj index 79c06ac..f5f3263 100644 --- a/test/book/match_test.clj +++ b/test/book/match_test.clj @@ -2,6 +2,9 @@ (:require [midje.sweet :refer :all] [book.match :refer :all])) +(def PersistentSortedList? (comp #(= % book.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}) @@ -101,4 +104,21 @@ 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}))) \ No newline at end of file + 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) => book.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/book/test_PersistentSortedList.clj b/test/book/test_PersistentSortedList.clj index 621003d..c6d66d0 100644 --- a/test/book/test_PersistentSortedList.clj +++ b/test/book/test_PersistentSortedList.clj @@ -30,6 +30,9 @@ (fact "drop" (drop 2 (sorted-list 1 2 3)) => (sorted-list 3) (drop 3 (sorted-list 1 2 3)) => (list)) + (fact "empty" + (empty (sorted-list 2 3 4)) => (sorted-list) + (into (empty (sorted-list-by #(- %2 %1) 2 3 4)) [20 30]) => (sorted-list-by #(- %2 %1) 20 30)) (fact "reduce" (reduce + (sorted-list 1 2 3)) => 6 (reduce + 10 (sorted-list 1 2 3)) => 16)