coinbase-pro-client/test/coinbase_pro/client_test.clj

338 lines
14 KiB
Clojure
Raw Permalink Normal View History

2022-06-07 09:44:49 -07:00
(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?))))))