338 lines
14 KiB
Clojure
338 lines
14 KiB
Clojure
(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?))))))
|