From 9c8b6cccf7532b73a2717085bb3bf76e162cc817 Mon Sep 17 00:00:00 2001 From: niten Date: Tue, 7 Jun 2022 09:44:49 -0700 Subject: [PATCH] Working with tests! --- deps.edn | 20 +- src/{coinbase-pro => coinbase_pro}/client.clj | 84 +++-- .../client/core.clj | 0 src/{coinbase-pro => coinbase_pro}/order.clj | 0 test/coinbase_pro/client_test.clj | 337 ++++++++++++++++++ test/coinbase_pro/order_test.clj | 77 ++++ test/coinbase_pro/test_helpers.clj | 49 +++ 7 files changed, 528 insertions(+), 39 deletions(-) rename src/{coinbase-pro => coinbase_pro}/client.clj (82%) rename src/{coinbase-pro => coinbase_pro}/client/core.clj (100%) rename src/{coinbase-pro => coinbase_pro}/order.clj (100%) create mode 100644 test/coinbase_pro/client_test.clj create mode 100644 test/coinbase_pro/order_test.clj create mode 100644 test/coinbase_pro/test_helpers.clj diff --git a/deps.edn b/deps.edn index dcb9ef5..4048677 100644 --- a/deps.edn +++ b/deps.edn @@ -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"] + } + } } diff --git a/src/coinbase-pro/client.clj b/src/coinbase_pro/client.clj similarity index 82% rename from src/coinbase-pro/client.clj rename to src/coinbase_pro/client.clj index 25198c2..30a045d 100644 --- a/src/coinbase-pro/client.clj +++ b/src/coinbase_pro/client.clj @@ -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)))] @@ -35,10 +35,10 @@ (-> (.doFinal hmac (.getBytes msg)) (base64-encode-string))))) -(s/def ::secret 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,28 +152,28 @@ :size :settled :created_at}] - (do (ensure-keys required-keys order) - (reify order/Order - (id [_] (-> order :id to-uuid)) - (currency [_] (-> order :product_id product-currency)) - (limit? [_] (-> order :type keyword (= :limit))) - (market? [_] (-> order :type keyword (= :market))) - (stop? [_] (-> order :stop nil? not)) - (sell? [_] (-> order :side keyword (= :sell))) - (buy? [_] (-> order :side keyword (= :buy))) - (stop-loss? [_] (-> order :stop keyword (= :loss))) - (stop-gain? [_] (-> order :stop keyword (= :entry))) - (filled? [_] (-> order :done_reason keyword (= :filled))) - (price [_] (-> order :price bigdec)) - (stop-price [_] (some-> order :stop_price bigdec)) - (size [_] (-> order :size bigdec)) - (settled? [_] (-> order :settled)) - (done? [_] (-> order :status keyword (= :done))) - (cancelled? [_] (-> order :done_reason keyword (= :canceled))) - (fees [_] (some-> order :fill_fees bigdec)) - (created [_] (-> order :created_at parse-timestamp)) - (completed [_] (some-> order :done_at parse-timestamp)) - (get-raw [_] order))))) + (ensure-keys required-keys order) + (reify order/Order + (id [_] (-> order :id to-uuid)) + (currency [_] (-> order :product_id product-currency)) + (limit? [_] (-> order :type keyword (= :limit))) + (market? [_] (-> order :type keyword (= :market))) + (stop? [_] (-> order :stop nil? not)) + (sell? [_] (-> order :side keyword (= :sell))) + (buy? [_] (-> order :side keyword (= :buy))) + (stop-loss? [_] (-> order :stop keyword (= :loss))) + (stop-gain? [_] (-> order :stop keyword (= :entry))) + (filled? [_] (-> order :done_reason keyword (= :filled))) + (price [_] (-> order :price bigdec)) + (stop-price [_] (some-> order :stop_price bigdec)) + (size [_] (-> order :size bigdec)) + (settled? [_] (-> order :settled)) + (done? [_] (-> order :status keyword (= :done))) + (cancelled? [_] (-> order :done_reason keyword (= :canceled))) + (fees [_] (some-> order :fill_fees bigdec)) + (created [_] (-> order :created_at parse-timestamp)) + (completed [_] (some-> order :done_at parse-timestamp)) + (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}))) diff --git a/src/coinbase-pro/client/core.clj b/src/coinbase_pro/client/core.clj similarity index 100% rename from src/coinbase-pro/client/core.clj rename to src/coinbase_pro/client/core.clj diff --git a/src/coinbase-pro/order.clj b/src/coinbase_pro/order.clj similarity index 100% rename from src/coinbase-pro/order.clj rename to src/coinbase_pro/order.clj diff --git a/test/coinbase_pro/client_test.clj b/test/coinbase_pro/client_test.clj new file mode 100644 index 0000000..6e477f8 --- /dev/null +++ b/test/coinbase_pro/client_test.clj @@ -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?)))))) diff --git a/test/coinbase_pro/order_test.clj b/test/coinbase_pro/order_test.clj new file mode 100644 index 0000000..2869aca --- /dev/null +++ b/test/coinbase_pro/order_test.clj @@ -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)}))) diff --git a/test/coinbase_pro/test_helpers.clj b/test/coinbase_pro/test_helpers.clj new file mode 100644 index 0000000..cbbd623 --- /dev/null +++ b/test/coinbase_pro/test_helpers.clj @@ -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)))))