Browse Source

Initial, still broken, checkin

master
niten 2 months ago
commit
21d07b383c
  1. 8
      .gitignore
  2. 15
      deps.edn
  3. 397
      deps.nix
  4. 36
      flake.nix
  5. 293
      src/coinbase-pro/client.clj
  6. 5
      src/coinbase-pro/client/core.clj
  7. 89
      src/coinbase-pro/order.clj
  8. 39
      src/exchange/account.clj
  9. 28
      src/exchange/client.clj
  10. 8
      src/exchange/common.clj
  11. 57
      src/exchange/order.clj
  12. 9
      src/exchange/ticker.clj

8
.gitignore

@ -0,0 +1,8 @@
.DS_Store
.idea
*.log
tmp/
.cpcache/
.nrepl-port
target/

15
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"
}
}
}

397
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 ];
}
];
}

36
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 ]; };
});
}

293
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)

5
src/coinbase-pro/client/core.clj

@ -0,0 +1,5 @@
(ns coinbase-pro.client.core)
(defn -main [_]
(println "Not Implemented!")
(System/exit 1))

89
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)))

39
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?)

28
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?)

8
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?)

57
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?))

9
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]))
Loading…
Cancel
Save