Working with tests!

This commit is contained in:
niten 2022-06-07 09:44:49 -07:00
parent 21d07b383c
commit 9c8b6cccf7
7 changed files with 528 additions and 39 deletions

View File

@ -9,7 +9,25 @@
org.fudo/fudo-clojure {
:git/url "https://git.fudo.org/fudo-public/fudo-clojure.git"
:sha "047f5d531c8d1493880313d13bbff6da88b0a4b8"
:sha "6f80b1de51cdd92bb04857edc562a076f6820a2a"
}
}
:aliases {
:test {
:extra-paths ["test"]
:extra-deps {
io.github.cognitect-labs/test-runner {
:git/url "https://github.com/cognitect-labs/test-runner.git"
:sha "dfb30dd6605cb6c0efc275e1df1736f6e90d4d73"
}
}
:main-opts ["-m" "cognitect.test-runner"]
:exec-fn cognitect.test-runner.api/test
}
:lint {
:replace-deps { clj-kondo/clj-kondo {:mvn/version "2022.04.25"} }
:main-opts ["-m" "clj-kondo.main" "--lint" "src"]
}
}
}

View File

@ -11,7 +11,7 @@
[coinbase-pro.order :as order-req]
[fudo-clojure.common :refer [base64-decode base64-encode-string ensure-conform instant-to-epoch-timestamp to-uuid parse-timestamp]]
[fudo-clojure.common :refer [base64-decode base64-encode-string ensure-conform instant-to-epoch-timestamp to-uuid parse-timestamp pthru]]
[fudo-clojure.http.client :as http]
[fudo-clojure.http.request :as req]
[fudo-clojure.logging :as log]
@ -19,15 +19,15 @@
(def signature-algo "HmacSHA256")
(defn- build-path [& path-elems]
(str "/" (str/join "/" (map to-path-elem path-elems))))
(defn- to-path-elem [el]
(cond (keyword? el) (name el)
(uuid? el) (.toString el)
(string? el) el
:else (throw (ex-info (str "Bad path element: " el) {}))))
(defn- build-path [& path-elems]
(str "/" (str/join "/" (map to-path-elem path-elems))))
(defn- make-signature-generator [key]
(let [hmac (doto (javax.crypto.Mac/getInstance signature-algo)
(.init (javax.crypto.spec.SecretKeySpec. key signature-algo)))]
@ -36,9 +36,9 @@
(base64-encode-string)))))
(s/def ::secret string?)
(s/def ::passphrase string?)
(s/def ::key string?)
(s/def ::hostname string?)
(s/def ::profile-id uuid?)
(s/def ::trade-id uuid?)
@ -137,7 +137,7 @@
(defn- ensure-keys [ks m]
(let [diff (set/difference ks (set (keys m)))]
(when (not (empty? diff))
(when (seq diff)
(throw (ex-info (str "missing keys: "
(str/join "," diff))
{:missing-keys diff
@ -152,7 +152,7 @@
:size
:settled
:created_at}]
(do (ensure-keys required-keys order)
(ensure-keys required-keys order)
(reify order/Order
(id [_] (-> order :id to-uuid))
(currency [_] (-> order :product_id product-currency))
@ -173,7 +173,7 @@
(fees [_] (some-> order :fill_fees bigdec))
(created [_] (-> order :created_at parse-timestamp))
(completed [_] (some-> order :done_at parse-timestamp))
(get-raw [_] order)))))
(get-raw [_] order))))
(defn- reify-ticker [currency ticker]
(reify ticker/Ticker
@ -185,7 +185,7 @@
(volume [_] (-> ticker :volume bigdec))))
(defn- reify-exchange-client [{client ::http/client
hostname ::common/hostname
hostname ::hostname
logger ::log/logger}]
(let [request! (fn [req] (http/execute-request! client (req/with-host req hostname)))]
(reify client/ExchangeClient
@ -196,8 +196,11 @@
(map-success (client/get-ticker! self currency)
ticker/price)))))
(defn- reify-exchange-account-client [{:keys [http/client common/hostname] :as opts}]
(let [public-client (reify-exchange-client opts)
(defn- reify-exchange-account-client [opts]
(let [{client ::http/client
hostname ::hostname
logger ::log/logger} opts
public-client (reify-exchange-client opts)
request! (fn [req] (http/execute-request! client (req/with-host req hostname)))
before (fn [a b] (.isBefore a b))
reify-orders (comp (partial sort-by order/created before)
@ -285,9 +288,14 @@
(order-req/with-size (bigdec size)))))
(comp to-uuid :id))))))
(defn connect
([client] ()))
(defn connect [& args]
(reify-client (apply build-connection args)))
(s/fdef connect :ret ::client/client)
(defn connect [& {:keys [hostname logger credentials]
:or { logger (log/print-logger) }}]
(if credentials
(let [authenticator (make-request-authenticator credentials)]
(reify-exchange-account-client {::http/client (http/json-client :authenticator authenticator
:logger logger)
::log/logger logger
::hostname hostname}))
(reify-exchange-client {::http/client (http/json-client :logger logger)
::log/logger logger
::hostname hostname})))

View File

@ -0,0 +1,337 @@
(ns coinbase-pro.client-test
(:require [clojure.test :as t :refer [deftest is testing]]
[clojure.data.json :as json]
[clojure.string :as str]
[fudo-clojure.http.client :as http]
[fudo-clojure.http.request :as req]
[fudo-clojure.common :as common]
[fudo-clojure.result :refer [unwrap map-success]]
[exchange.account :as acct]
[exchange.client :as client]
[exchange.order :as order]
[coinbase-pro.client :as cb]
[fudo-clojure.logging :as log])
(:use coinbase-pro.client))
(def cb-passphrase "6%CRf$zL!b@toz")
(def cb-secret "J/KtQJgu/TRsWigrqnc2KFBxdOTC5L8woaubzmj7G9hfxv9N5tcgsZ+yd9DvqmroJt1nzcy5Hsrk3IipcdFucA==")
(def cb-key "57c45fb2a300dd00f7a02af8353af81b")
(def test-time-str "2022-02-06T10:34:47.123456Z")
(def test-time (common/parse-timestamp test-time-str))
(defmacro is-true [& body]
`(clojure.test/is (= true ~@body)))
(defmacro is-false [& body]
`(clojure.test/is (= false ~@body)))
(defmacro is-success? [& body]
`(is-true (fudo-clojure.result/success? ~@body)))
(defmacro is-failure? [& body]
`(is-true (fudo-clojure.result/failure? ~@body)))
(defn gen-uuid [] (java.util.UUID/randomUUID))
(defn base-request []
(-> (req/base-request)
(req/with-timestamp test-time)
(req/as-get)
(req/with-host "some.dummy.host")))
(defn- http-client-returning [response]
(-> (reify http/HTTPClient
(get! [_ _] response)
(post! [_ _] response)
(delete! [_ _] response))
(http/client:wrap-results)
(http/client:jsonify)))
(defn- http-client-throwing [e]
(-> (reify http/HTTPClient
(get! [_ _] (throw e))
(post! [_ _] (throw e))
(delete! [_ _] (throw e)))
(http/client:wrap-results)
(http/client:jsonify)))
(defn sample [coll]
(nth coll (rand-int (count coll))))
(defn date-to-instant [date]
(-> (java.time.LocalDateTime/parse (str date "T00:00"))
(.atZone (java.time.ZoneId/systemDefault))
(.toInstant)))
(defn instant-to-epoch [i]
(.getEpochSecond i))
(defn random-instant [& {:keys [start end]
:or {start (date-to-instant "2010-01-01")
end (date-to-instant "2022-05-01")}}]
(let [s (instant-to-epoch start)
e (instant-to-epoch end)]
(java.time.Instant/ofEpochSecond (+ s (rand-int (- e s))))))
(defn gen-order
"A response from Coinbase"
([] (gen-order {}))
([overrides]
(let [created-at (random-instant)]
(merge {
:id (gen-uuid)
:product_id (sample ["BTC-USD" "ADA-USD" "ETH-USD"])
:type (sample [:limit :market])
:side (sample [:buy :sell])
:done_reason (sample [nil :done :cancelled])
:price (bigdec (rand 100000))
:stop-price (sample [nil (rand 100000)])
:size (bigdec (rand 1000))
:settled (sample [true false])
:fill_fees (bigdec (rand 100))
:completed (sample [true false])
:created_at (str created-at)
:done_at (str (sample [nil (random-instant :end created-at)]))
}
overrides))))
(defn make-public-client [http-client]
(#'coinbase-pro.client/reify-exchange-client {::http/client http-client}))
(defn public-client-returning [resp]
(make-public-client (http-client-returning resp)))
(defn public-client-throwing [e]
(make-public-client (http-client-throwing e)))
(defn resp [& {:keys [code body]}]
{:status (.toString (or code 200))
:body (json/write-str (or body {}))})
(deftest test-auth-headers
(let [creds {::cb/key cb-key
::cb/secret cb-secret
::cb/passphrase cb-passphrase}
make-request-authenticator #'coinbase-pro.client/make-request-authenticator
authenticator (make-request-authenticator creds)]
(testing "timestamp-header"
(is (= (str (common/instant-to-epoch-timestamp test-time))
(-> (authenticator (-> (base-request)
(req/as-get)
(req/with-path "/one/two")
(req/with-body "hello")))
::req/headers
::cb/cb-access-timestamp))))
(testing "access-key-header"
(is (= cb-key
(-> (authenticator (-> (base-request)
(req/as-get)
(req/with-path "/one/two")
(req/with-body "hello")))
::req/headers
::cb/cb-access-key))))
(testing "access-passphrase"
(is (= cb-passphrase
(-> (authenticator (-> (base-request)
(req/as-get)
(req/with-path "/one/two")
(req/with-body "hello")))
::req/headers
::cb/cb-access-passphrase))))
(testing "signature"
(let [signer (#'coinbase-pro.client/make-signature-generator (common/base64-decode cb-secret))
ts (common/instant-to-epoch-timestamp test-time)]
(let [cat-str (str ts "GET/one/two?")
sig (signer cat-str)]
(is (= sig
(-> (authenticator (-> (base-request)
(req/as-get)
(req/with-path "/one/two")))
::req/headers
::cb/cb-access-sign))))
(let [cat-str (str ts "GET/one/two?hello")
sig (signer cat-str)]
(is (= sig
(-> (authenticator (-> (base-request)
(req/as-get)
(req/with-path "/one/two")
(req/with-body "hello")))
::req/headers
::cb/cb-access-sign))))
(let [cat-str (str ts "GET/one/two?")
sig (signer cat-str)]
(is (not (= sig
(-> (authenticator (-> (base-request)
(req/as-get)
(req/with-path "/one/two")
(req/with-body "oops")))
::req/headers
::cb/cb-access-sign))))
(is (not (= sig
(-> (authenticator (-> (base-request)
(req/as-get)
(req/with-path "/one/two")
(req/with-timestamp (java.time.Instant/now))))
::req/headers
::cb/cb-access-sign)))))))))
(defn make-account-client [http-client]
(#'coinbase-pro.client/reify-exchange-account-client {::http/client http-client}))
(defn account-client-returning [resp]
(make-account-client (http-client-returning resp)))
(defn account-client-throwing [e]
(make-account-client (http-client-throwing e)))
(deftest test-accounts
(let [balance (bigdec (rand 10000000))
available (bigdec (rand balance))
hold (bigdec (- balance available))
id (gen-uuid)
profile-id (gen-uuid)
currency "BTC"
result [{:id (str id)
:profile_id (str profile-id)
:currency currency
:balance (str balance)
:available (str available)
:hold (str hold)
:trading_enabled true}
{:id (str (gen-uuid))
:profile_id (str (gen-uuid))
:currency "ETH"
:balance (str balance)
:available (str available)
:hold (str hold)
:trading_enabled true}]
response (resp :body result)]
(testing "get-accounts!"
(is-success? (client/get-accounts! (account-client-returning response)))
(is-failure? (client/get-accounts! (account-client-throwing (RuntimeException. "oops!"))))
(is-true (every? acct/account?
(vals (unwrap (client/get-accounts! (account-client-returning response))))))
(is (= balance
(-> (account-client-returning response)
(client/get-accounts!)
(unwrap)
:btc
(acct/balance))))
(is (= available
(-> (account-client-returning response)
(client/get-accounts!)
(unwrap)
:btc
(acct/available))))
(is (= hold
(-> (account-client-returning response)
(client/get-accounts!)
(unwrap)
:btc
(acct/hold))))
(is (= (-> currency str/lower-case keyword)
(-> (account-client-returning response)
(client/get-accounts!)
(unwrap)
:btc
(acct/currency)))))
(testing "get-account!"
(is-success? (client/get-account! (account-client-returning response) :btc))
(is-success? (client/get-account! (account-client-returning response) :eth))
(is-failure? (client/get-account! (account-client-returning response) :ada))
(is-true (-> (client/get-account! (account-client-returning response) :btc)
(unwrap)
(acct/account?))))))
(deftest test-cancel-order
(let [order-id (gen-uuid)]
(is-success? (client/cancel-order! (account-client-returning (resp :body order-id )) order-id))
(is-failure? (client/cancel-order! (account-client-throwing (RuntimeException. "oops!")) order-id))
(is-failure? (client/cancel-order! (account-client-throwing (ex-info "not found" { :status 404 }))
order-id))
(is (= 404
(http/status
(client/cancel-order! (account-client-throwing (ex-info "Not found!" { :status 404 }))
order-id))))))
(defmacro test-order-property [order-fn val params]
`(is (= ~val (-> (client/get-order! (account-client-returning (resp :body (gen-order ~params))) (gen-uuid))
(unwrap)
~order-fn))))
(deftest test-get-orders
(testing "get-order!"
(let [order-id (gen-uuid)
respond-with-order (fn [m] (account-client-returning
(resp :body (gen-order (merge { :id order-id } m)))))]
(is-success? (client/get-order! (respond-with-order {}) order-id))
(is-failure? (client/get-order! (account-client-throwing (RuntimeException. "oops!")) order-id))
(is-failure? (client/get-order! (account-client-throwing (ex-info "not found" { :status 404 })) order-id))
(is (= 404
(http/status
(client/get-order! (account-client-throwing (ex-info "not found" { :status 404 })) order-id))))
(test-order-property order/id order-id { :id order-id })
(let [price (bigdec (rand 100000))
size (bigdec (rand 100000))
fees (bigdec (rand 1000))
stop-price (bigdec (rand 100000))]
(test-order-property order/price price { :price price })
(test-order-property order/size size { :size size })
(test-order-property order/fees fees { :fill_fees fees })
(test-order-property order/stop-price stop-price { :stop_price stop-price })
(test-order-property order/stop-price nil { :stop_price nil }))
(test-order-property order/currency :btc { :product_id "BTC-USD" })
(test-order-property order/currency :eth { :product_id "ETH-USD" })
(is-failure? (-> (client/get-order! (respond-with-order { :product_id "BTC-USD?" }) order-id)
(map-success order/currency)))
(test-order-property order/limit? true { :type :limit })
(test-order-property order/market? false { :type :limit })
(test-order-property order/limit? false { :type :market })
(test-order-property order/market? true { :type :market })
(test-order-property order/stop? true { :stop :stop })
(test-order-property order/stop? true { :stop :entry })
(test-order-property order/stop? false { :stop nil })
(test-order-property order/stop-loss? true { :stop :loss })
(test-order-property order/stop-gain? false { :stop :loss })
(test-order-property order/stop-loss? false { :stop :entry })
(test-order-property order/stop-gain? true { :stop :entry })
(test-order-property order/stop-loss? false { :stop nil })
(test-order-property order/stop-gain? false { :stop nil })
(test-order-property order/sell? true { :side :sell })
(test-order-property order/buy? false { :side :sell })
(test-order-property order/sell? false { :side :buy })
(test-order-property order/buy? true { :side :buy })
(test-order-property order/filled? true { :done_reason :filled })
(test-order-property order/filled? false { :done_reason nil })
(test-order-property order/created test-time { :created_at test-time-str})
(test-order-property order/completed test-time { :done_at test-time-str})
(test-order-property order/completed nil { :done_at nil })))
(testing "get-orders!"
(let [orders (map (fn [_] (gen-order)) (range 10))]
(is-success? (client/get-orders! (account-client-returning (resp :body orders)) :btc))
(is (= (count orders)
(count (unwrap (client/get-orders! (account-client-returning (resp :body orders)) :btc)))))
(is-true (->> (client/get-orders! (account-client-returning (resp :body orders)) :btc)
(unwrap)
(every? order/order?))))))

View File

@ -0,0 +1,77 @@
(ns coinbase-pro.order-test
(:require [coinbase-pro.order :as cb-order]
[coinbase-pro.test-helpers :refer [gen-order
gen-limit-order
gen-limit-buy
gen-limit-sell
gen-stop-gain
gen-stop-loss]]
[fudo-clojure.common :refer [is-valid? is-invalid? sample]]
[clojure.test :as t :refer [deftest testing is]]))
(defn gen-amount [& {:keys [min max]
:or {min 0 max (+ min 10000000)}}]
(bigdec (+ (rand (- max min)) min)))
(deftest test-order
(testing "order"
(is-valid? ::cb-order/base-order (gen-order))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/product-id :btc-usd }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/product-id "USD-BTC" }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/product-id "BTCBTCBTC-USD" }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/product-id "BTC-USDC" }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/product-id nil }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/side :oops }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/side nil }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/price "12345" }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/price 5 }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/price nil }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/size "12345" }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/size 5 }))
(is-invalid? ::cb-order/base-order (gen-order { ::cb-order/size nil }))))
(testing "buy-order"
(is-valid? ::cb-order/buy-order (gen-order { ::cb-order/side :buy }))
(is-invalid? ::cb-order/buy-order (gen-order { ::cb-order/side :sell })))
(testing "sell-order"
(is-valid? ::cb-order/sell-order (gen-order { ::cb-order/side :sell }))
(is-invalid? ::cb-order/sell-order (gen-order { ::cb-order/side :buy })))
(testing "limit-buy-order"
(is-valid? ::cb-order/limit-buy-order (gen-limit-buy))
(is-invalid? ::cb-order/limit-buy-order (gen-limit-buy { ::cb-order/side :sell }))
(is-invalid? ::cb-order/limit-buy-order (gen-limit-buy { ::cb-order/type :market })))
(testing "limit-sell-order"
(is-valid? ::cb-order/limit-sell-order (gen-limit-sell))
(is-invalid? ::cb-order/limit-sell-order (gen-limit-sell { ::cb-order/side :buy }))
(is-invalid? ::cb-order/limit-sell-order (gen-limit-sell { ::cb-order/type :market })))
(testing "stop-gain-order"
(is-valid? ::cb-order/stop-gain-order (gen-stop-gain))
(is-invalid? ::cb-order/stop-gain-order (gen-stop-gain {::cb-order/stop :loss}))
(is-invalid? ::cb-order/stop-gain-order (gen-stop-gain {::cb-order/stop-price nil}))
(is-invalid? ::cb-order/stop-gain-order (gen-stop-gain {::cb-order/stop-price "12345"}))
(is-invalid? ::cb-order/stop-gain-order (gen-stop-gain {::cb-order/stop-price 5}))
(is-invalid? ::cb-order/stop-gain-order (gen-stop-gain {::cb-order/stop-price (bigdec 8)
::cb-order/price (bigdec 7)})))
(testing "stop-loss-order"
(is-valid? ::cb-order/stop-loss-order (gen-stop-loss))
(is-invalid? ::cb-order/stop-loss-order (gen-stop-loss {::cb-order/stop :entry}))
(is-invalid? ::cb-order/stop-loss-order (gen-stop-loss {::cb-order/stop-price nil}))
(is-invalid? ::cb-order/stop-loss-order (gen-stop-loss {::cb-order/stop-price "12345"}))
(is-invalid? ::cb-order/stop-loss-order (gen-stop-loss {::cb-order/stop-price 5}))
(is-invalid? ::cb-order/stop-loss-order (gen-stop-loss {::cb-order/stop-price (bigdec 7)
::cb-order/price (bigdec 8)})))

View File

@ -0,0 +1,49 @@
(ns coinbase-pro.test-helpers
(:require [clojure.test :as t]
[clojure.spec.alpha :as s]
[fudo-clojure.common :refer [sample]]
[coinbase-pro.order :as cb-order]))
(defn gen-amount [& {:keys [min max]
:or {min 0 max (+ min 10000000)}}]
(bigdec (+ (rand (- max min)) min)))
(defn gen-order
([] (gen-order {}))
([ks] (merge {::cb-order/product-id (sample ["BTC-USD" "ETH-USD" "ADA-USD"])
::cb-order/type (sample [:limit :market])
::cb-order/side (sample [:buy :sell])
::cb-order/price (gen-amount)
::cb-order/size (gen-amount)}
ks)))
(defn gen-limit-order
([ks] (gen-order (merge { ::cb-order/type :limit } ks)))
([] (gen-limit-order {})))
(defn gen-limit-buy
([ks] (gen-limit-order (merge { ::cb-order/side :buy } ks)))
([] (gen-limit-buy {})))
(defn gen-limit-sell
([ks] (gen-limit-order (merge { ::cb-order/side :sell } ks)))
([] (gen-limit-sell {})))
(defn gen-stop-gain
([] (gen-stop-gain {}))
([ks]
(let [stop-price (gen-amount)
price (gen-amount :min stop-price)]
(gen-limit-buy (merge {::cb-order/stop :entry
::cb-order/stop-price stop-price
::cb-order/price price}
ks)))))
(defn gen-stop-loss
([] (gen-stop-loss {}))
([ks]
(let [stop-price (gen-amount)
price (gen-amount :max stop-price)]
(gen-limit-sell (merge {::cb-order/stop :loss
::cb-order/stop-price stop-price
::cb-order/price price}
ks)))))