commit
21d07b383c
12 changed files with 984 additions and 0 deletions
-
8.gitignore
-
15deps.edn
-
397deps.nix
-
36flake.nix
-
293src/coinbase-pro/client.clj
-
5src/coinbase-pro/client/core.clj
-
89src/coinbase-pro/order.clj
-
39src/exchange/account.clj
-
28src/exchange/client.clj
-
8src/exchange/common.clj
-
57src/exchange/order.clj
-
9src/exchange/ticker.clj
@ -0,0 +1,8 @@ |
|||
.DS_Store |
|||
.idea |
|||
*.log |
|||
tmp/ |
|||
|
|||
.cpcache/ |
|||
.nrepl-port |
|||
target/ |
@ -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" |
|||
} |
|||
} |
|||
} |
@ -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 ]; |
|||
} |
|||
|
|||
]; |
|||
} |
|||
|
@ -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 ]; }; |
|||
}); |
|||
} |
@ -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) |
@ -0,0 +1,5 @@ |
|||
(ns coinbase-pro.client.core) |
|||
|
|||
(defn -main [_] |
|||
(println "Not Implemented!") |
|||
(System/exit 1)) |
@ -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))) |
@ -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?) |
@ -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?) |
@ -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?) |
@ -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?)) |
@ -0,0 +1,9 @@ |
|||
(ns exchange.ticker) |
|||
|
|||
(defprotocol Ticker |
|||
(currency [self]) |
|||
(price [self]) |
|||
(tick-time [self]) |
|||
(bid [self]) |
|||
(ask [self]) |
|||
(volume [self])) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue