commit 21d07b383c92f2225b09615c17266b2986fd5a09 Author: niten Date: Mon Jun 6 09:37:23 2022 -0700 Initial, still broken, checkin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e11263c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +.idea +*.log +tmp/ + +.cpcache/ +.nrepl-port +target/ diff --git a/deps.edn b/deps.edn new file mode 100644 index 0000000..dcb9ef5 --- /dev/null +++ b/deps.edn @@ -0,0 +1,15 @@ +{ + :paths ["src"] + :deps { + org.clojure/clojure { :mvn/version "1.10.3" } + org.clojure/core.async { :mvn/version "1.5.640" } + org.clojure/data.json { :mvn/version "2.4.0" } + + clj-http/clj-http { :mvn/version "3.12.3" } + + org.fudo/fudo-clojure { + :git/url "https://git.fudo.org/fudo-public/fudo-clojure.git" + :sha "047f5d531c8d1493880313d13bbff6da88b0a4b8" + } + } + } diff --git a/deps.nix b/deps.nix new file mode 100644 index 0000000..7277456 --- /dev/null +++ b/deps.nix @@ -0,0 +1,397 @@ +# generated by clj2nix-1.1.0-rc +{ fetchMavenArtifact, fetchgit, lib }: + +let repos = [ + "https://repo1.maven.org/maven2/" + "https://repo.clojars.org/" ]; + + in rec { + makePaths = {extraClasspaths ? []}: + if (builtins.typeOf extraClasspaths != "list") + then builtins.throw "extraClasspaths must be of type 'list'!" + else (lib.concatMap (dep: + builtins.map (path: + if builtins.isString path then + path + else if builtins.hasAttr "jar" path then + path.jar + else if builtins.hasAttr "outPath" path then + path.outPath + else + path + ) + dep.paths) + packages) ++ extraClasspaths; + makeClasspaths = {extraClasspaths ? []}: + if (builtins.typeOf extraClasspaths != "list") + then builtins.throw "extraClasspaths must be of type 'list'!" + else builtins.concatStringsSep ":" (makePaths {inherit extraClasspaths;}); + packageSources = builtins.map (dep: dep.src) packages; + packages = [ + rec { + name = "data.json/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "data.json"; + groupId = "org.clojure"; + sha512 = "04b7c0c90cb26d643a0b3e7e1ffa2d2d423e977c1454ee5ea7c2e75547ecbc113838df17b797902a975f5ea2184a81a45b605a4d82970805e2bbb02feebc578d"; + version = "2.4.0"; + + }; + paths = [ src ]; + } + + rec { + name = "clojure/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clojure"; + groupId = "org.clojure"; + sha512 = "4bb567b9262d998f554f44e677a8628b96e919bc8bcfb28ab2e80d9810f8adf8f13a8898142425d92f3515e58c57b16782cff12ba1b5ffb38b7d0ccd13d99bbc"; + version = "1.10.3"; + + }; + paths = [ src ]; + } + + rec { + name = "commons-codec/commons-codec"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "commons-codec"; + groupId = "commons-codec"; + sha512 = "da30a716770795fce390e4dd340a8b728f220c6572383ffef55bd5839655d5611fcc06128b2144f6cdcb36f53072a12ec80b04afee787665e7ad0b6e888a6787"; + version = "1.15"; + + }; + paths = [ src ]; + } + + rec { + name = "tools.analyzer/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "tools.analyzer"; + groupId = "org.clojure"; + sha512 = "c51752a714848247b05c6f98b54276b4fe8fd44b3d970070b0f30cd755ac6656030fd8943a1ffd08279af8eeff160365be47791e48f05ac9cc2488b6e2dfe504"; + version = "1.1.0"; + + }; + paths = [ src ]; + } + + rec { + name = "core.specs.alpha/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "core.specs.alpha"; + groupId = "org.clojure"; + sha512 = "c1d2a740963896d97cd6b9a8c3dcdcc84459ea66b44170c05b8923e5fbb731b4b292b217ed3447bbc9e744c9a496552f77a6c38aea232e5e69f8faa627dea4b5"; + version = "0.2.56"; + + }; + paths = [ src ]; + } + + rec { + name = "spec.alpha/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "spec.alpha"; + groupId = "org.clojure"; + sha512 = "0740dc3a755530f52e32d27139a9ebfd7cbdb8d4351c820de8d510fe2d52a98acd6e4dfc004566ede3d426e52ec98accdca1156965218f269e60dd1cd4242a73"; + version = "0.2.194"; + + }; + paths = [ src ]; + } + + rec { + name = "httpasyncclient/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpasyncclient"; + groupId = "org.apache.httpcomponents"; + sha512 = "0a80db5dbf772f02d02ba6c7c163e8da9517dd7195714b495acb845c429580c1fc926d3e71c115e75be8c145651dce2fdfa0dc380132f7809c14b3ad95492aee"; + version = "4.1.4"; + + }; + paths = [ src ]; + } + + rec { + name = "tools.analyzer.jvm/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "tools.analyzer.jvm"; + groupId = "org.clojure"; + sha512 = "6764305bd18a5b7bddd7e50b037cbcdb4f5cf61606faa92353bfb4fdb89dc9055530c665e102cd7e17b808f3461255bcc8c88a7b46d5af9bec8d6eaf7000ae7d"; + version = "1.2.0"; + + }; + paths = [ src ]; + } + + rec { + name = "slingshot/slingshot"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "slingshot"; + groupId = "slingshot"; + sha512 = "ff2b2a27b441d230261c7f3ec8c38aa551865e05ab6438a74bd12bfcbc5f6bdc88199d42aaf5932b47df84f3d2700c8f514b9f4e9b5da28d29da7ff6b09a7fb5"; + version = "0.12.2"; + + }; + paths = [ src ]; + } + + rec { + name = "httpcore-nio/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpcore-nio"; + groupId = "org.apache.httpcomponents"; + sha512 = "002af5f72b68a4ff1b1ff46b788013283d195e1d62ee1d7b102aa930b30f77f7e215a6d18edbea0fccd18fb1fa3a66cc4aef6070d72d6d1886f0044dfe0e16c7"; + version = "4.4.10"; + + }; + paths = [ src ]; + } + + rec { + name = "commons-io/commons-io"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "commons-io"; + groupId = "commons-io"; + sha512 = "72040ed293a083f979c3f23b00c359195cf0e4c227a9cb962d99804cbe07d86e24d2864aa8c533bb79e4ad1f83d3d17f290c8c24630410eb80734d6d1266e7ec"; + version = "2.8.0"; + + }; + paths = [ src ]; + } + + rec { + name = "clj-http/clj-http"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clj-http"; + groupId = "clj-http"; + sha512 = "9884557d4f38068cb3234aec80acc0de8f9716645529693ffd9bd6db8221f5d1cf9e2d1b8bf7c7df4215d71372b02d83043ebf8fc27dc422552b32c9bdba1602"; + version = "3.12.3"; + + }; + paths = [ src ]; + } + + rec { + name = "asm/org.ow2.asm"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "asm"; + groupId = "org.ow2.asm"; + sha512 = "40614e658138f2eb95bc26999545f996794c622c4d68efb9e10093743504c4b58bf22590767bc6bd93b77cdfb202c507144ba867bbc8b54d74fe7621cbc55e3a"; + version = "5.2"; + + }; + paths = [ src ]; + } + + rec { + name = "httpcore/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpcore"; + groupId = "org.apache.httpcomponents"; + sha512 = "f16a652f4a7b87dbf7cb16f8590d54a3f719c4c7b2f8883ce59db2d73be4701b64f2ca8a2c45aca6a5dbeaddeedff0c280a03722f70c076e239b645faa54eff9"; + version = "4.4.14"; + + }; + paths = [ src ]; + } + + rec { + name = "httpclient-cache/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpclient-cache"; + groupId = "org.apache.httpcomponents"; + sha512 = "e150e8dc49c8c9972d8b324b56bb292b15e2f0e686f1292c4edac975615dfb16e5edb8ab325e614732a7d43a03061ca4fe93fe1e1f7487851a4d4d3af50a61f9"; + version = "4.5.13"; + + }; + paths = [ src ]; + } + + rec { + name = "clj-tuple/clj-tuple"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clj-tuple"; + groupId = "clj-tuple"; + sha512 = "dd626944d0aba679a21b164ed0c77ea84449359361496cba810f83b9fdeab751e5889963888098ce4bf8afa112dbda0a46ed60348a9c01ad36a2e255deb7ab6d"; + version = "0.2.2"; + + }; + paths = [ src ]; + } + + rec { + name = "riddley/riddley"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "riddley"; + groupId = "riddley"; + sha512 = "b478ecba9d1ab9d38c84a42354586fcece763000907b40c97bc43c0f16dc560b0860144efe410193cb3b7cb0149fbc1724fdd737cc3ba53de23618f5b30e6f9f"; + version = "0.1.12"; + + }; + paths = [ src ]; + } + + rec { + name = "commons-logging/commons-logging"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "commons-logging"; + groupId = "commons-logging"; + sha512 = "ed00dbfabd9ae00efa26dd400983601d076fe36408b7d6520084b447e5d1fa527ce65bd6afdcb58506c3a808323d28e88f26cb99c6f5db9ff64f6525ecdfa557"; + version = "1.2"; + + }; + paths = [ src ]; + } + + rec { + name = "httpclient/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpclient"; + groupId = "org.apache.httpcomponents"; + sha512 = "3567739186e551f84cad3e4b6b270c5b8b19aba297675a96bcdff3663ff7d20d188611d21f675fe5ff1bfd7d8ca31362070910d7b92ab1b699872a120aa6f089"; + version = "4.5.13"; + + }; + paths = [ src ]; + } + + (rec { + name = "org.fudo/fudo-clojure"; + src = fetchgit { + name = "fudo-clojure"; + url = "https://git.fudo.org/fudo-public/fudo-clojure.git"; + rev = "047f5d531c8d1493880313d13bbff6da88b0a4b8"; + sha256 = "1lbc7nf0qdb97znn6nl46q0489caxlsiki2apw4isfx8m14d095m"; + }; + paths = map (path: src + path) [ + "/src" + ]; + }) + + rec { + name = "tools.reader/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "tools.reader"; + groupId = "org.clojure"; + sha512 = "290a2d98b2eec08a8affc2952006f43c0459c7e5467dc454f5fb5670ea7934fa974e6be19f7e7c91dadcfed62082d0fbcc7788455b7446a2c9c5af02f7fc52b6"; + version = "1.3.2"; + + }; + paths = [ src ]; + } + + rec { + name = "potemkin/potemkin"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "potemkin"; + groupId = "potemkin"; + sha512 = "5abc050bf7ff0b27d8c45aaa5e378201980815b711b2db99735db73304576c17e285026ea48a714bf0b0df7ad7a008de38b7d182cdc0e8989f4be1e6b3afa8aa"; + version = "0.4.5"; + + }; + paths = [ src ]; + } + + rec { + name = "core.memoize/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "core.memoize"; + groupId = "org.clojure"; + sha512 = "37308fcbbe64d0a2802917ef5a589075f81086d63e08c71a9a1b648b73dd362e5bdc8f756084fde1f4b1964ba82a6dc06b2119460281b7949a271d82e6a47a7e"; + version = "1.0.236"; + + }; + paths = [ src ]; + } + + rec { + name = "camel-snake-kebab/camel-snake-kebab"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "camel-snake-kebab"; + groupId = "camel-snake-kebab"; + sha512 = "589d34b500560b7113760a16bfb6f0ccd8f162a1ce8c9bc829495432159ba9c95aebf6bc43aa126237a0525806a205a05f9910122074902b659e7fd151d176b1"; + version = "0.4.2"; + + }; + paths = [ src ]; + } + + rec { + name = "data.priority-map/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "data.priority-map"; + groupId = "org.clojure"; + sha512 = "fb2d703468fb6d5f28c38f25e8e7acdaf02d2fa1ac23c14a9ff065873e88c9b74e155e73e5069436d674d7ef8547f01bc9777b7ae3b9dcde67cbd327d4a20c06"; + version = "1.0.0"; + + }; + paths = [ src ]; + } + + rec { + name = "httpmime/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpmime"; + groupId = "org.apache.httpcomponents"; + sha512 = "e1b0ee84bce78576074dc1b6836a69d8f5518eade38562e6890e3ddaa72b7f54bf735c8e2286142c58cddf45f745da31261e5d73b7d8092eb6ecfb20946eb36c"; + version = "4.5.13"; + + }; + paths = [ src ]; + } + + rec { + name = "core.cache/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "core.cache"; + groupId = "org.clojure"; + sha512 = "6e4e126f23b20120c50a4dbefbe1b3b9bd98f0a7b8fa83affa267ff7f0de09542d2727243859a1ea346bda5b782d4ae0110f6c2b169c298261707a1fdadaedb0"; + version = "1.0.207"; + + }; + paths = [ src ]; + } + + rec { + name = "core.async/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "core.async"; + groupId = "org.clojure"; + sha512 = "11de341de544951f1c944fca67610d024f562a87127bb9a7095aaa8b5ae0e7c4e7ddaebbe2567ade7ff988beda804835d8f5eb6b2b0a0c0d6766e697fe817523"; + version = "1.5.640"; + + }; + paths = [ src ]; + } + + ]; + } + \ No newline at end of file diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..945444c --- /dev/null +++ b/flake.nix @@ -0,0 +1,36 @@ +{ + description = "Coinbase Pro Client."; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-22.05"; + utils.url = "github:numtide/flake-utils"; + clj-nix = { + url = "github:jlesquembre/clj-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, utils, clj-nix, ... }: + utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages."${system}"; + cljpkgs = clj-nix.packages."${system}"; + update-deps = pkgs.writeShellScriptBin "update-deps.sh" '' + ${clj-nix.packages."${system}".deps-lock}/bin/deps-lock + ''; + in { + packages = { + coinbase-pro-client = cljpkgs.mkCljBin { + projectSrc = ./.; + name = "org.fudo/coinbase-pro.client"; + main-ns = "coinbase-pro.client.core"; + jdkRunner = pkgs.jdk17_headless; + }; + }; + + defaultPackage = self.packages."${system}".coinbase-pro-client; + + devShell = + pkgs.mkShell { buildInputs = with pkgs; [ clojure update-deps ]; }; + }); +} diff --git a/src/coinbase-pro/client.clj b/src/coinbase-pro/client.clj new file mode 100644 index 0000000..25198c2 --- /dev/null +++ b/src/coinbase-pro/client.clj @@ -0,0 +1,293 @@ +(ns coinbase-pro.client + (:require [clojure.set :as set] + [clojure.spec.alpha :as s] + [clojure.string :as str] + + [exchange.account :as acct] + [exchange.client :as client] + [exchange.common :as common] + [exchange.order :as order] + [exchange.ticker :as ticker] + + [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.http.client :as http] + [fudo-clojure.http.request :as req] + [fudo-clojure.logging :as log] + [fudo-clojure.result :refer [map-success bind success exception-failure]])) + +(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- make-signature-generator [key] + (let [hmac (doto (javax.crypto.Mac/getInstance signature-algo) + (.init (javax.crypto.spec.SecretKeySpec. key signature-algo)))] + (fn [msg] + (-> (.doFinal hmac (.getBytes msg)) + (base64-encode-string))))) + +(s/def ::secret string?) + +(s/def ::passphrase string?) +(s/def ::key string?) + +(s/def ::profile-id uuid?) +(s/def ::trade-id uuid?) +(s/def ::order-id uuid?) + +(s/def ::credentials + (s/keys :req [::authenticator + ::key + ::passphrase])) + +(s/def ::hostname string?) + +(s/def ::connection + (s/keys :req [::hostname ::http/client] + :opt [::log/logger])) + +(s/def ::authenticated-connection + (s/and ::connection + (s/keys :req [::credentials]))) + +(defn- make-request-authenticator + [{key ::key secret ::secret passphrase ::passphrase}] + (let [sign (make-signature-generator (base64-decode secret))] + (fn [req] + (let [epoch-timestamp (-> req req/timestamp instant-to-epoch-timestamp str) + req-str (str epoch-timestamp + (-> req req/method name) + (-> req req/request-path) + (-> req req/body (or ""))) + signature (sign req-str)] + (req/with-headers req + {::cb-access-timestamp epoch-timestamp + ::cb-access-key key + ::cb-access-passphrase passphrase + ::cb-access-sign signature}))))) + +(def lower-case-keyword (comp keyword str/lower-case)) + +(defn- currency-product [currency] + (str (-> currency name str/upper-case) + "-USD")) + +(defn- product-currency [product] + (if-let [currency (some-> (re-matches #"^([A-Z]{2,5})-USD$" product) + (get 1) + (lower-case-keyword))] + currency + (throw (ex-info (str "not a valid product_id: " product) + {:product product})))) + +(defn- accounts-request [] + (-> (req/base-request) + (req/as-get) + (req/with-path (build-path :accounts)))) + +(defn- reify-account [acct] + (reify acct/CurrencyAccount + (currency [_] (-> acct :currency lower-case-keyword)) + (balance [_] (-> acct :balance bigdec)) + (hold [_] (-> acct :hold bigdec)) + (available [_] (-> acct :available bigdec)))) + +(defn- order-request [order-id] + (-> (req/base-request) + (req/as-get) + (req/with-path (build-path :orders order-id)))) + +(defn- cancel-order-request [order-id] + (-> (req/base-request) + (req/as-delete) + (req/with-path (build-path :orders order-id)))) + +(defn- currency-orders-request + ([currency] (currency-orders-request currency {})) + ([currency query] (-> (req/base-request) + (req/as-get); + (req/with-path (build-path :orders)) + (req/with-query-params + (merge query { :product_id (currency-product currency) }))))) + +(defn- create-order-request [order] + (-> (req/base-request) + (req/as-post) + (req/with-path (build-path :orders)) + (req/with-body-params (ensure-conform ::order-req/order order)))) +(s/fdef create-order-request + :args (s/cat :params ::order-req/order) + :ret ::req/request) + +(defn- ticker-request [currency] + (-> (req/base-request) + (req/as-get) + (req/with-path (build-path :products + (currency-product currency) + :ticker)))) + +(defn- ensure-keys [ks m] + (let [diff (set/difference ks (set (keys m)))] + (when (not (empty? diff)) + (throw (ex-info (str "missing keys: " + (str/join "," diff)) + {:missing-keys diff + :map m}))))) + +(defn- reify-order [order] + (let [required-keys #{:id + :product_id + :type + :side + :price + :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))))) + +(defn- reify-ticker [currency ticker] + (reify ticker/Ticker + (currency [_] currency) + (price [_] (-> ticker :price bigdec)) + (tick-time [_] (-> ticker :time parse-timestamp)) + (bid [_] (-> ticker :bid bigdec)) + (ask [_] (-> ticker :ask bigdec)) + (volume [_] (-> ticker :volume bigdec)))) + +(defn- reify-exchange-client [{client ::http/client + hostname ::common/hostname + logger ::log/logger}] + (let [request! (fn [req] (http/execute-request! client (req/with-host req hostname)))] + (reify client/ExchangeClient + (get-ticker! [_ currency] + (map-success (request! (ticker-request currency)) + (partial reify-ticker currency))) + (get-market-price! [self currency] + (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) + 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) + (partial map reify-order)) + accounts-map (fn [accts] (into {} (map (juxt acct/currency identity) accts)))] + (reify + client/ExchangeClient + (get-ticker! [_ currency] (client/get-ticker! public-client currency)) + + (get-market-price! [_ currency] (client/get-market-price! public-client currency)) + + client/ExchangeAccountClient + (get-accounts! [_] + (map-success (request! (accounts-request)) + (comp accounts-map (partial map reify-account)))) + + (get-account! [this currency] + (bind (client/get-accounts! this) + (fn [accts] + (if-let [acct (get accts currency)] + (success acct) + (exception-failure (ex-info (str "no account for currency: " currency) + {:currency currency + :existing-accounts accts})))))) + + (get-order! [_ order-id] + (map-success (request! (order-request order-id)) + reify-order)) + + (get-orders! [_ currency] + (map-success (request! (currency-orders-request currency)) + reify-orders)) + + (get-incomplete-orders! [_ currency] + (map-success (request! (currency-orders-request currency { ::order/status [:open :pending] })) + reify-orders)) + + (get-completed-orders! [_ currency] + (map-success (request! (currency-orders-request currency { ::order/status [:done] })) + reify-orders)) + + (get-completed-limit-orders! [self currency] + (map-success (client/get-completed-orders! self currency) + (comp reify-orders (partial filter order/limit?)))) + + (get-completed-limit-buy-orders! [self currency] + (map-success (client/get-completed-limit-orders! self currency) + (comp reify-orders (partial filter order/buy?)))) + + (get-completed-limit-sell-orders! [self currency] + (map-success (client/get-completed-limit-orders! self currency) + (comp reify-orders (partial filter order/sell?)))) + + (cancel-order! [_ order-id] + (map-success (request! (cancel-order-request order-id)) + to-uuid)) + + (create-stop-loss-order! [_ currency stop-price sell-price size] + (map-success (request! (create-order-request (-> (order-req/base-order (currency-product currency)) + (order-req/as-stop-loss (bigdec stop-price)) + (order-req/with-price (bigdec sell-price)) + (order-req/with-size (bigdec size))))) + (comp to-uuid :id))) + + (create-stop-gain-order! [_ currency stop-price buy-price size] + (map-success (request! (create-order-request (-> (order-req/base-order (currency-product currency)) + (order-req/as-stop-gain (bigdec stop-price)) + (order-req/with-price (bigdec buy-price)) + (order-req/with-size (bigdec size))))) + (comp to-uuid :id))) + + (create-limit-sell-order! [_ currency sell-price size] + (map-success (request! (create-order-request (-> (order-req/base-order (currency-product currency)) + (order-req/as-limit) + (order-req/as-sell) + (order-req/with-price (bigdec sell-price)) + (order-req/with-size (bigdec size))))) + (comp to-uuid :id))) + + (create-limit-buy-order! [_ currency buy-price size] + (map-success (request! (create-order-request (-> (order-req/base-order (currency-product currency)) + (order-req/as-limit) + (order-req/as-buy) + (order-req/with-price (bigdec buy-price)) + (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) diff --git a/src/coinbase-pro/client/core.clj b/src/coinbase-pro/client/core.clj new file mode 100644 index 0000000..67f3607 --- /dev/null +++ b/src/coinbase-pro/client/core.clj @@ -0,0 +1,5 @@ +(ns coinbase-pro.client.core) + +(defn -main [_] + (println "Not Implemented!") + (System/exit 1)) diff --git a/src/coinbase-pro/order.clj b/src/coinbase-pro/order.clj new file mode 100644 index 0000000..6483917 --- /dev/null +++ b/src/coinbase-pro/order.clj @@ -0,0 +1,89 @@ +(ns coinbase-pro.order + (:require [clojure.spec.alpha :as s] + [fudo-clojure.common :refer [*->]])) + +;; Anything other than USD is an error for now +(defn- product-id? [product] + (and (string? product) + (not (nil? (re-matches #"^[A-Z]{2,5}-USD$" product))))) + +(defn- must-be [k v] + (fn [o] (= (k o) v))) + +(defn- ensure-relationship [pred k0 k1] + (fn [o] (pred (k0 o) (k1 o)))) + +;; Order details are going to be exchange-specific, I'll put them here for now. +(s/def ::product-id product-id?) +(s/def ::type #{:limit :market}) +(s/def ::side #{:buy :sell}) +(s/def ::stop #{:entry :loss}) +(s/def ::stop-price decimal?) +(s/def ::price decimal?) +(s/def ::size decimal?) + +(s/def ::base-order + (s/keys :req [::product-id + ::type + ::side + ::price + ::size])) + +(s/def ::buy-order + (s/and ::base-order (must-be ::side :buy))) + +(s/def ::sell-order + (s/and ::base-order (must-be ::side :sell))) + +(s/def ::limit-buy-order + (s/and ::buy-order (must-be ::type :limit))) + +(s/def ::limit-sell-order + (s/and ::sell-order (must-be ::type :limit))) + +(s/def ::stop-gain-order + (s/and ::limit-buy-order + (s/keys :req [::stop ::stop-price]) + (must-be ::stop :entry) + (ensure-relationship < ::stop-price ::price))) + +(s/def ::stop-loss-order + (s/and ::limit-sell-order + (s/keys :req [::stop ::stop-price]) + (must-be ::stop :loss) + (ensure-relationship > ::stop-price ::price))) + +(s/def ::order + (s/or :buy ::buy-order + :sell ::sell-order + :limit-buy ::limit-buy-order + :limit-sell ::limit-sell-order + :stop-gain ::stop-gain-order + :stop-loss ::stop-loss-order)) + +(defn base-order [product] + { ::product-id product }) + +(def as-limit (*-> (assoc ::type :limit))) +(def as-market (*-> (assoc ::type :market))) +(def as-buy (*-> (assoc ::side :buy))) +(def as-sell (*-> (assoc ::side :sell))) + +(defn with-price [o price] + (assoc o ::price price)) +(defn with-size [o size] + (assoc o ::size size)) + +(defn as-stop-loss [o stop-price] + (-> o + (as-limit) + (as-sell) + (assoc ::stop-price stop-price) + (assoc ::stop :loss))) + +(defn as-stop-gain [o stop-price] + (-> o + (as-limit) + (as-buy) + (assoc ::stop-price stop-price) + (assoc ::stop :entry))) diff --git a/src/exchange/account.clj b/src/exchange/account.clj new file mode 100644 index 0000000..f0e665c --- /dev/null +++ b/src/exchange/account.clj @@ -0,0 +1,39 @@ +(ns exchange.account + (:require [clojure.spec.alpha :as s] + [exchange.common :as common])) + +(s/def ::balance decimal?) +(s/def ::hold decimal?) +(s/def ::available decimal?) + +(defprotocol CurrencyAccount + (currency [self]) + (balance [self]) + (hold [self]) + (available [self])) + +(def account? (partial satisfies? CurrencyAccount)) + +(defn currency-account? [curr acct] + (= curr (currency acct))) + +(defn account-balance [accts curr] + (balance (get accts curr))) + +(s/def ::acct account?) + +(s/fdef currency + :args (s/cat :acct ::acct) + :ret ::common/currency) + +(s/fdef balance + :args (s/cat :acct ::acct) + :ret decimal?) + +(s/fdef hold + :args (s/cat :acct ::acct) + :ret decimal?) + +(s/fdef available + :args (s/cat :acct ::acct) + :ret decimal?) diff --git a/src/exchange/client.clj b/src/exchange/client.clj new file mode 100644 index 0000000..af422d6 --- /dev/null +++ b/src/exchange/client.clj @@ -0,0 +1,28 @@ +(ns exchange.client + (:require [clojure.spec.alpha :as s])) + +(defprotocol ExchangeClient + (get-ticker! [client currency]) + (get-market-price! [client currency])) + +(defprotocol ExchangeAccountClient + (get-order! [client order-id]) + (get-orders! [client currency]) + (get-incomplete-orders! [client currency]) + (get-completed-orders! [client currency]) + (get-completed-limit-orders! [client currency]) + (get-completed-limit-buy-orders! [client currency]) + (get-completed-limit-sell-orders! [client currency]) + (cancel-order! [client order-id]) + (get-accounts! [client]) + (get-account! [client currency]) + (create-stop-loss-order! [client currency stop-price sell-price size]) + (create-stop-gain-order! [client currency stop-price buy-price size]) + (create-limit-sell-order! [client currency sell-price size]) + (create-limit-buy-order! [client currency buy-price size])) + +(def client? (partial satisfies? ExchangeClient)) +(def account-client? (partial satisfies? ExchangeAccountClient)) + +(s/def ::client client?) +(s/def ::account-client account-client?) diff --git a/src/exchange/common.clj b/src/exchange/common.clj new file mode 100644 index 0000000..cce952e --- /dev/null +++ b/src/exchange/common.clj @@ -0,0 +1,8 @@ +(ns exchange.common + (:require [clojure.spec.alpha :as s])) + +(s/def ::amount decimal?) +(s/def ::balance decimal?) +(s/def ::currency keyword?) +(s/def ::timestamp (partial instance? java.time.Instant)) +(s/def ::hostname string?) diff --git a/src/exchange/order.clj b/src/exchange/order.clj new file mode 100644 index 0000000..e2cf7f2 --- /dev/null +++ b/src/exchange/order.clj @@ -0,0 +1,57 @@ +(ns exchange.order + (:refer-clojure :exclude [type]) + (:require [clojure.spec.alpha :as s] + [exchange.common :as common])) + +(defprotocol Order + (id [order]) + (currency [order]) + (limit? [order]) + (market? [order]) + (stop? [order]) + (sell? [order]) + (buy? [order]) + (stop-loss? [order]) + (stop-gain? [order]) + (filled? [order]) + (price [order]) + (stop-price [order]) + (size [order]) + (settled? [order]) + (fees [order]) + (created [order]) + (completed [order]) + (done? [order]) + (cancelled? [order]) + (get-raw [order])) + +(def order? (partial satisfies? Order)) + +(s/def ::order order?) + +(s/def ::order-id uuid?) + +(defn- fn-order-to [type] + (s/fspec :args (s/cat :order ::order) + :ret type)) + +(s/def id (fn-order-to ::order-id)) +(s/def currency (fn-order-to ::common/currency)) +(s/def limit? (fn-order-to boolean?)) +(s/def market? (fn-order-to boolean?)) +(s/def stop? (fn-order-to boolean?)) +(s/def sell? (fn-order-to boolean?)) +(s/def buy? (fn-order-to boolean?)) +(s/def stop-loss? (fn-order-to boolean?)) +(s/def stop-gain? (fn-order-to boolean?)) +(s/def filled? (fn-order-to boolean?)) +(s/def price (fn-order-to ::common/amount)) +(s/def stop-price (fn-order-to (s/nilable ::common/amount))) +(s/def size (fn-order-to ::common/amount)) +(s/def cancelled? (fn-order-to boolean?)) +(s/def settled? (fn-order-to boolean?)) +(s/def fees (fn-order-to ::common/amount)) +(s/def created (fn-order-to ::common/timestamp)) +(s/def completed (fn-order-to ::common/timestamp)) +(s/def done? (fn-order-to boolean?)) +(s/def cancelled? (fn-order-to boolean?)) diff --git a/src/exchange/ticker.clj b/src/exchange/ticker.clj new file mode 100644 index 0000000..b14eb89 --- /dev/null +++ b/src/exchange/ticker.clj @@ -0,0 +1,9 @@ +(ns exchange.ticker) + +(defprotocol Ticker + (currency [self]) + (price [self]) + (tick-time [self]) + (bid [self]) + (ask [self]) + (volume [self]))