Fixed many bugs.

This commit is contained in:
didier amyot committed 2018-06-24 17:16:44 -04:00
1 parent ad924ca5c7
commit 174319293c
10 files changed
+222 -89

No files matched your search

+2 -1
View File
@@ -19,5 +19,6 @@
:plugins [[lein-midje "3.2.1"]] :plugins [[lein-midje "3.2.1"]]
:profiles { :profiles {
:uberjar {:aot :all} :uberjar {:aot :all}
:dev {:dependencies [[midje "1.9.1"]]}}) :dev {:jvm-opts ["-XX:-OmitStackTraceInFastThrow"]
:dependencies [[midje "1.9.1"]]}})
+2 -2
View File
@@ -1,7 +1,7 @@
(ns book.cli (ns book.cli
(:require [book.match :refer [sell?]] (:require [book.match :refer [sell?]]
[book.util :refer :all] [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 [book.order] ;; for spec side effect
[slingshot.slingshot :refer [try+ throw+]] [slingshot.slingshot :refer [try+ throw+]]
[clojure.edn :as edn] [clojure.edn :as edn]
@@ -42,7 +42,7 @@
(defn -main [& args] (defn -main [& args]
(let [initial-book empty-book (let [initial-book empty-book
incoming-order-channel (chan) 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 "trade > " trades-channel))
(go (print-chan "order > " new-book-order-channel)) (go (print-chan "order > " new-book-order-channel))
(loop [order (read-order)] (loop [order (read-order)]
+28 -22
View File
@@ -4,37 +4,43 @@
create-sell-book create-sell-book
match-order-in-book]] match-order-in-book]]
[book.util :refer :all] [book.util :refer :all]
[clojure.core.async :refer [chan go >!! <!!]])) [clojure.core.async :refer [chan go >!! <! <!! >! pub]]))
(def empty-book {:sell (create-sell-book) :buy (create-buy-book)}) (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) (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)] ;ff (println ">>=" same-type-book (type same-type-book));;
(when (seq trades) (doseq [trade trades] (>!! trades-channel trade))) ;ff (println ">>=" leftover-order (type leftover-order))
(when leftover-order (>!! new-book-order-channel 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])) [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] (defn process-sell-order-2 [{sell-book :sell buy-book :buy} order]
(let [[new-buy-book new-sell-book] (process-abstract-sell order buy-book sell-book trades-channel new-book-order-channel)] (let [[new-buy-book new-sell-book new-order trades] (process-abstract order buy-book sell-book)
{:sell new-sell-book :buy new-buy-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] (defn process-buy-order-2 [{sell-book :sell buy-book :buy} order]
(let [[new-sell-book new-buy-book] (process-abstract-sell order sell-book buy-book trades-channel new-book-order-channel)] (let [[new-sell-book new-buy-book new-order trades] (process-abstract order sell-book buy-book)
{:sell new-sell-book :buy new-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] (defn process-order-2 [book order]
(loop [order (<!! orders-channel) (if (sell? order)
book master-book] (process-sell-order-2 book order)
(let [process-order (if (sell? order) process-sell-order process-buy-order) (process-buy-order-2 book order)))
new-book (process-order order book trades-channel new-book-order-channel)]
(recur (<!! orders-channel) new-book))))
(defn process-orders-2 [initial-book incoming-order-channel]
(defn create-exchange-for-security [initial-book incoming-order-channel]
(let [trades-channel (chan) (let [trades-channel (chan)
new-book-order-channel (chan)] new-book-order-channel (chan)]
(go (process-orders initial-book incoming-order-channel trades-channel new-book-order-channel)) (go (loop [order (<! incoming-order-channel) book initial-book]
[trades-channel new-book-order-channel])) (let [[new-book new-order new-trades] (process-order-2 book order)]
(when (not (nil? new-order)) (>! new-book-order-channel new-order))
(doseq [trade new-trades] (>! trades-channel trade))
(recur (<!! incoming-order-channel) new-book))))
[(pub trades-channel :book.order/type) (pub new-book-order-channel :type)]))
(def create-exchange-for-security-2 process-orders-2)
+8 -19
View File
@@ -74,8 +74,14 @@
(defn compute-partial-order [order trade] (defn compute-partial-order [order trade]
(assoc order :quantity (- (:quantity order) (:book.order/quantity trade 0)))) (assoc order :quantity (- (:quantity order) (:book.order/quantity trade 0))))
(defn compute-new-book-after-trades [sell-book trades] (defn- drop-in-sorted-list [n sorted-list]
(let [new-book (drop (-> trades count dec) sell-book) (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)] possible-partial-trade (last trades)]
(if (partial-trade? possible-partial-trade (first new-book)) (if (partial-trade? possible-partial-trade (first new-book))
(conj (rest new-book) (compute-partial-order (first new-book) possible-partial-trade)) (conj (rest new-book) (compute-partial-order (first new-book) possible-partial-trade))
@@ -93,20 +99,3 @@
new-buy-order (leftover-order order trades)] new-buy-order (leftover-order order trades)]
[trades new-book new-buy-order])) [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)))))
;
;
+5 -1
View File
@@ -36,8 +36,12 @@
200) 200)
status)) 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}] (defn build-response [request {status :status body :body :as response}]
(let [[content-type serializer] (get-best-serializer request)] (let [[content-type serializer] (get-best-serializer request)]
{:body (serializer response) {:body (serializer (if (nil? body) response body))
:headers {"Content-Type" content-type} :headers {"Content-Type" content-type}
:status (get-status status request)})) :status (get-status status request)}))
+89 -23
View File
@@ -1,14 +1,15 @@
(ns book.web (ns book.web
(:require [book.exchange :refer [empty-book]] (: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.route :refer [not-found]]
[compojure.handler :refer [api]] [compojure.handler :refer [api]]
[compojure.core :refer :all] [compojure.core :refer :all]
[book.order] ;; for spec side effect [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] [clojure.spec.alpha :as s]
[slingshot.slingshot :refer [try+ throw+]] [slingshot.slingshot :refer [try+ throw+]]
[cheshire.core :refer [generate-string]] [clojure.core.async :refer [chan go go-loop close! <! >!! sub]]
[clojure.edn :as edn])) [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] (fn [request]
(build-response request {:securities (keys books)}))) (build-response request {:securities (keys exchange-apis)})))
(defn get-security [books]
(fn [{{sec-id-str :sec-id} :route-params body :body :as request}]))
(defn validate-order [asked-order] (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)) (if (not (s/valid? :book.order/order asked-order))
(throw+ {:status :4XX :headers {"Content-Type" "application/json"} :body "foo"})) (throw+ {:error-type :4XX
(assoc asked-order :book.order/id (swap! order-id inc))) :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-order [body] (defn get-body [body]
(try+ (try+
(-> body clojure.java.io/reader slurp edn/read-string) (-> body clojure.java.io/reader slurp edn/read-string)
(catch Exception e (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}] (fn [{{sec-id-str :sec-id} :route-params body :body :as request}]
(try+ (try+
(let [asked-order (get-order body) (let [asked-order (get-body body)
order (validate-order asked-order) exchange-api (get-exchange-api (keyword sec-id-str) exchange-apis)
sec-id (keyword sec-id-str) incoming-order-channel (:incoming-order-channel exchange-api)
book (get books sec-id)] order (validate-order asked-order (:sec-id exchange-api))]
(build-response request {:order order :book book})) (>!! incoming-order-channel order)
(build-response request order))
(catch [:error-type :4XX] e (build-response request e)) (catch [:error-type :4XX] e (build-response request e))
(catch [:error-type :5XX] 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 (<! chan)]
(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]
(routes (routes
(context "/api/1" [] (context "/api/1" []
(GET "/" [] api-description) (GET "/" [] api-description)
(GET "/securities" [] (securities books)) (GET "/securities" [] (securities exchange-apis))
(GET "/securities/:sec-id" [] (get-security books)) (GET "/securities/:sec-id" [] (get-security exchange-apis))
(POST "/securities/:sec-id" [] (create-order books))) (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"))) (not-found "not found")))
(defn -main [& args] (defn -main [& args]
(let [books {:foo empty-book :bar empty-book} (let [exchange-apis {:foo (create-exchange-api-for :foo) :bar (create-exchange-api-for :bar)}
api-routes (create-routes books)] api-routes (create-routes exchange-apis)]
(run-server (api api-routes) {:port 8080}))) (run-server (api api-routes) {:port 8080})))
+6 -4
View File
@@ -13,11 +13,11 @@ public class PersistentSortedList extends Obj implements Iterable, RandomAccess,
List, Counted{ List, Counted{
final private PersistentTreeMap impl; final private PersistentTreeMap impl;
final private int _count; final private int _count;
static public final PersistentSortedList EMPTY = new PersistentSortedList(null, PersistentTreeMap.EMPTY, 0); static private final PersistentSortedList EMPTY_DEFAULT_COMPARATOR = new PersistentSortedList(null, PersistentTreeMap.EMPTY, 0);
private int _hasheq = 0; private int _hasheq = 0;
public static PersistentSortedList create(ISeq items){ public static PersistentSortedList create(ISeq items){
PersistentSortedList ret = EMPTY; PersistentSortedList ret = EMPTY_DEFAULT_COMPARATOR;
for(; items != null; items = items.next()){ for(; items != null; items = items.next()){
ret = ret.cons(items.first()); ret = ret.cons(items.first());
} }
@@ -52,7 +52,9 @@ public class PersistentSortedList extends Obj implements Iterable, RandomAccess,
@Override @Override
public IPersistentCollection empty() { public IPersistentCollection empty() {
return PersistentSortedList.EMPTY; return new PersistentSortedList(null,
new PersistentTreeMap(null, this.impl.comp),
0);
} }
@Override @Override
@@ -142,7 +144,7 @@ public class PersistentSortedList extends Obj implements Iterable, RandomAccess,
@Override @Override
public ISeq more() { public ISeq more() {
ISeq s = next(); ISeq s = next();
return s == null ? PersistentSortedList.EMPTY : s; return s == null ? (PersistentSortedList) empty() : s;
} }
@Override @Override
+59 -17
View File
@@ -1,28 +1,70 @@
(ns book.exchange-test (ns book.exchange-test
(:import [book.PersistentSortedList])
(:require [midje.sweet :refer :all] (:require [midje.sweet :refer :all]
[book.exchange :refer :all] [book.exchange :refer :all]
[book.match :refer [create-buy-book [book.match :refer [create-buy-book
create-sell-book]] create-sell-book]]))
[clojure.core.async :refer [chan go >!! <!!]]))
(def PersistentSortedList? (comp #(= % book.PersistentSortedList) type))
(facts "the exchange as an whole" (facts "the exchange as an whole"
(fact "sell order followed by a buy order trivialities " (fact "process-sell-order should work when book empty"
(let [incoming-order-channel (chan) (let [book empty-book
trades-channel (chan) order {:type :sell :quantity 10 :price 2 :book.order/id 1}
new-book-order-channel (chan) expt-new-sell-book [order]
master-book {:sell (create-sell-book) :buy (create-buy-book)}] [{new-sell-book :sell new-buy-book :buy} new-order new-trades] (process-sell-order-2 book order)]
(go (process-orders master-book incoming-order-channel trades-channel new-book-order-channel)) new-sell-book => expt-new-sell-book
(>!! incoming-order-channel {:type :sell :quantity 10 :price 2}) new-buy-book => empty?
(<!! new-book-order-channel) => {:type :sell :quantity 10 :price 2} new-order => order
(>!! incoming-order-channel {:type :buy :quantity 20 :price 2}) new-trades => empty?))
(<!! trades-channel) => #:book.order{:book-id nil
:order-id nil
:price 2
:quantity 10
:type :sell}
(<!! new-book-order-channel) => {:type :buy :quantity 10 :price 2})))
(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)))
+20
View File
@@ -2,6 +2,9 @@
(:require [midje.sweet :refer :all] (:require [midje.sweet :refer :all]
[book.match :refer :all])) [book.match :refer :all]))
(def PersistentSortedList? (comp #(= % book.PersistentSortedList) type))
(facts "matching buy order in sell book" (facts "matching buy order in sell book"
(fact "Simplest case I." (fact "Simplest case I."
(let [sell-book (create-sell-book {:type :sell :quantity 10 :price 2 :book.order/id 101}) (let [sell-book (create-sell-book {:type :sell :quantity 10 :price 2 :book.order/id 101})
@@ -102,3 +105,20 @@
trades => [#:book.order{:quantity 2 :price 14 :book-id 116 :order-id 118 :type :buy} 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}] #: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}))) 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 => {})))
+3
View File
@@ -30,6 +30,9 @@
(fact "drop" (fact "drop"
(drop 2 (sorted-list 1 2 3)) => (sorted-list 3) (drop 2 (sorted-list 1 2 3)) => (sorted-list 3)
(drop 3 (sorted-list 1 2 3)) => (list)) (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" (fact "reduce"
(reduce + (sorted-list 1 2 3)) => 6 (reduce + (sorted-list 1 2 3)) => 6
(reduce + 10 (sorted-list 1 2 3)) => 16) (reduce + 10 (sorted-list 1 2 3)) => 16)