├── .gitignore ├── tests ├── balance.lisp ├── time.lisp ├── cryptography.lisp ├── assets.lisp ├── asset-pairs.lisp ├── server-time.lisp ├── http.lisp ├── ticker.lisp ├── spread.lisp ├── trade-volume.lisp ├── trades.lisp ├── depth.lisp └── ohlc.lisp ├── src ├── globals.lisp ├── cryptography.lisp ├── http.lisp ├── time.lisp └── main.lisp ├── LICENSE ├── cl-kraken.asd ├── .travis.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.FASL 2 | *.fasl 3 | *.lisp-temp 4 | *.dfsl 5 | *.pfsl 6 | *.d64fsl 7 | *.p64fsl 8 | *.lx64fsl 9 | *.lx32fsl 10 | *.dx64fsl 11 | *.dx32fsl 12 | *.fx64fsl 13 | *.fx32fsl 14 | *.sx64fsl 15 | *.sx32fsl 16 | *.wx64fsl 17 | *.wx32fsl 18 | 19 | *~ 20 | .#* 21 | 22 | .emacs* 23 | -------------------------------------------------------------------------------- /tests/balance.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/balance.lisp 2 | 3 | (defpackage #:cl-kraken/tests/balance 4 | (:use #:cl #:cl-kraken #:rove)) 5 | (in-package #:cl-kraken/tests/balance) 6 | 7 | (deftest balance 8 | (testing "when passed no keyword params" 9 | (ok (equal (cl-kraken:balance) `(:OBJ ("error"))))) 10 | (testing "when passed RAW T" 11 | (ok (string= (cl-kraken:balance :raw t) "{\"error\":[]}")))) 12 | -------------------------------------------------------------------------------- /src/globals.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/src/globals.lisp 2 | 3 | (in-package #:cl-user) 4 | (defpackage #:cl-kraken/src/globals 5 | (:documentation "CL-Kraken global parameters, variables and constants.") 6 | (:use #:cl)) 7 | (in-package #:cl-kraken/src/globals) 8 | 9 | ;;; User API key and secret 10 | 11 | (defparameter *api-key* 12 | "J9G291yReQZ4k7JKqaSsSwKCwcQBDdwzQq92z/I8MOu5j3g3SAAJo04c" 13 | "This is an empty account for testing. Replace this value with your API key.") 14 | (defparameter *api-secret* 15 | "Y9T6tmWcRsWryvmi7kpxQcYD5MuMSYpDTCVI1/j4aMnEr+J3QLrU66RTp6KAGmvrsrbs2ycCgjQgELgY9GU5FQ==" 16 | "This is an empty account for testing. Replace this vaule with your API secret.") 17 | 18 | ;;; Global Parameters 19 | 20 | (defparameter +api-scheme+ "https") 21 | (defparameter +api-host+ "api.kraken.com") 22 | (defparameter +api-version+ "0") 23 | (defparameter +version+ (concatenate 'string "/" +api-version+ "/")) 24 | (defparameter +api-public-path+ (concatenate 'string +version+ "public/" )) 25 | (defparameter +api-private-path+ (concatenate 'string +version+ "private/" )) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 Jon Atack (jon@atack.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/time.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/time.lisp 2 | 3 | (defpackage #:cl-kraken/tests/time 4 | (:use #:cl #:cl-kraken #:rove)) 5 | (in-package #:cl-kraken/tests/time) 6 | 7 | (deftest +one-million+ 8 | (testing "evaluates to the integer 1,000,000" 9 | (ok (eql cl-kraken/src/time::+one-million+ 1000000)))) 10 | 11 | (deftest +one-thousand+ 12 | (testing "evaluates to the integer 1,000" 13 | (ok (eql cl-kraken/src/time::+one-thousand+ 1000)))) 14 | 15 | (deftest unix-time-in-microseconds 16 | (testing "is an integer" 17 | (ok (integerp (cl-kraken/src/time:unix-time-in-microseconds)))) 18 | (testing "is 51 bits in length" 19 | (ok (= 51 (integer-length (cl-kraken/src/time:unix-time-in-microseconds)))))) 20 | 21 | (deftest generate-kraken-nonce 22 | (testing "is a string" 23 | (ok (simple-string-p (cl-kraken/src/time:generate-kraken-nonce)))) 24 | (testing "is 16 characters in length" 25 | (ok (= 16 (length (cl-kraken/src/time:generate-kraken-nonce))))) 26 | (testing "is continually increasing" 27 | (let ((old-nonce (parse-integer (cl-kraken/src/time:generate-kraken-nonce)))) 28 | (sleep 0.001) 29 | (ok (> (parse-integer (cl-kraken/src/time:generate-kraken-nonce)) old-nonce))))) 30 | -------------------------------------------------------------------------------- /cl-kraken.asd: -------------------------------------------------------------------------------- 1 | ;;;; -*- mode: lisp; syntax: common-lisp; indent-tabs-mode: nil -*- 2 | ;;;; cl-kraken.asd 3 | ;;;; ASDF system definition for CL-Kraken 4 | 5 | (defsystem "cl-kraken" 6 | :name "CL-Kraken" 7 | :author "Jon Atack " 8 | :description "A Common Lisp API client for the Kraken exchange" 9 | :homepage "https://github.com/jonatack/cl-kraken" 10 | :license "MIT" 11 | :version "0.0.3" 12 | :class :package-inferred-system 13 | :depends-on ("cl-kraken/src/main") 14 | :in-order-to ((test-op (test-op "cl-kraken/tests")))) 15 | 16 | (defsystem "cl-kraken/tests" 17 | :name "CL-Kraken tests" 18 | :author "Jon Atack " 19 | :description "Unit tests for CL-Kraken" 20 | :class :package-inferred-system 21 | :depends-on ("rove" 22 | "cl-kraken/tests/time" 23 | "cl-kraken/tests/cryptography" 24 | "cl-kraken/tests/http" 25 | "cl-kraken/tests/asset-pairs" 26 | "cl-kraken/tests/assets" 27 | "cl-kraken/tests/depth" 28 | "cl-kraken/tests/ohlc" 29 | "cl-kraken/tests/server-time" 30 | "cl-kraken/tests/spread" 31 | "cl-kraken/tests/ticker" 32 | "cl-kraken/tests/trades" 33 | "cl-kraken/tests/balance" 34 | "cl-kraken/tests/trade-volume") 35 | :perform (test-op (op c) (symbol-call :rove '#:run c))) 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: common-lisp 2 | 3 | sudo: false 4 | 5 | os: 6 | - osx 7 | # - linux 8 | 9 | env: 10 | global: 11 | - PATH=~/.roswell/bin:$PATH 12 | - ROSWELL_INSTALL_DIR=$HOME/.roswell 13 | - COVERAGE_EXCLUDE=tests # for Prove or Rove 14 | - ROSWELL_BRANCH=master # use Roswell master for latest updates 15 | matrix: 16 | - LISP=sbcl COVERAGE=1 17 | # - LISP=ccl COVERAGE=1 18 | # - LISP=abcl 19 | # - LISP=clisp 20 | # - LISP=cmucl 21 | # - LISP=ecl 22 | 23 | matrix: 24 | fast_finish: true 25 | allow_failures: 26 | - env: LISP=ccl COVERAGE=1 27 | - env: LISP=clisp 28 | - env: LISP=abcl 29 | - env: LISP=alisp 30 | - env: LISP=cmucl 31 | - env: LISP=ecl 32 | 33 | addons: 34 | apt: 35 | packages: 36 | # - libc6-i386 # needed for some implementations 37 | # - openjdk-8-jre # needed for ABCL 38 | 39 | cache: 40 | directories: 41 | - $HOME/.roswell 42 | - $HOME/.config/common-lisp 43 | 44 | install: 45 | - curl -L https://raw.githubusercontent.com/roswell/roswell/$ROSWELL_BRANCH/scripts/install-for-ci.sh | sh 46 | - git clone --depth=1 git://github.com/soemraws/parse-float.git ~/common-lisp/parse-float 47 | - ros install fukamachi/rove # Use Rove master for latest updates 48 | - if [ "$COVERAGE" = "1" ]; then ros install fukamachi/cl-coveralls; fi 49 | 50 | script: 51 | - rove cl-kraken.asd 52 | 53 | after_script: 54 | - if [ "$COVERAGE" = "1" ]; then 55 | coveralls report/; 56 | fi 57 | -------------------------------------------------------------------------------- /src/cryptography.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/src/cryptography.lisp 2 | 3 | (in-package :cl-user) 4 | (defpackage #:cl-kraken/src/cryptography 5 | (:documentation 6 | "Cryptographic functions for authenticating private POST requests.") 7 | (:use #:cl) 8 | (:import-from #:ironclad 9 | #:digest-sequence 10 | #:make-hmac 11 | #:update-hmac 12 | #:hmac-digest) 13 | (:import-from #:cl-base64 14 | #:base64-string-to-usb8-array 15 | #:usb8-array-to-base64-string) 16 | (:import-from #:quri 17 | #:url-encode-params) 18 | (:export #:signature)) 19 | (in-package #:cl-kraken/src/cryptography) 20 | 21 | (defun signature (path nonce data secret) 22 | "Signature generated from the HMAC SHA512 of a message and key: 23 | message = (PATH + SHA256(NONCE + POST DATA)) in octets 24 | key = base64-decoded SECRET in octets 25 | Before returning, the signature is converted from octets to a base64 string." 26 | (check-type path (and simple-string (not null))) 27 | (check-type nonce (and simple-string (not null))) 28 | (check-type data (cons)) 29 | (check-type secret (and simple-string (not null))) 30 | (let ((message (message path nonce data)) 31 | (key (base64-string-to-usb8-array secret))) 32 | (usb8-array-to-base64-string (hmac-sha512 message key)))) 33 | 34 | (defun message (path nonce data) 35 | "(PATH + SHA256(NONCE + POST DATA)) in octets." 36 | (concatenate '(simple-array (unsigned-byte 8) (*)) 37 | (map '(simple-array (unsigned-byte 8) (*)) 'char-code path) 38 | (hash-sha256 39 | (map '(simple-array (unsigned-byte 8) (*)) 'char-code 40 | (concatenate 'string nonce (url-encode-params data)))))) 41 | 42 | (defun hmac-sha512 (message secret) 43 | "Evaluates to an HMAC SHA512 signature. Inputs and output in octets." 44 | (check-type message (vector (unsigned-byte 8))) 45 | (check-type secret (vector (unsigned-byte 8))) 46 | (let ((hmac (make-hmac secret :sha512))) 47 | (update-hmac hmac message) 48 | (hmac-digest hmac))) 49 | 50 | (defun hash-sha256 (message) 51 | "Evaluates to an SHA256 digest of the message. Input and output in octets." 52 | (check-type message (vector (unsigned-byte 8))) 53 | (digest-sequence :sha256 message)) 54 | -------------------------------------------------------------------------------- /tests/cryptography.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/cryptography.lisp 2 | 3 | (defpackage #:cl-kraken/tests/cryptography 4 | (:use #:cl #:cl-kraken #:rove)) 5 | (in-package #:cl-kraken/tests/cryptography) 6 | 7 | (deftest signature 8 | (let* ((path "/0/private/Balance") 9 | (nonce "1234567890123456789") 10 | (data `(("pair" . "xbteur, xbtusd") ("nonce" . ,nonce))) 11 | (secret "The quick brown fox jumped over the lazy dog") 12 | (expected (concatenate 'string 13 | "Nkov7OdxRPxqRW9YiyTScW3LnKNNJJWO5JIzUY9/NHKjgu" 14 | "P+hj5vGqkGtqvpL7Cg5dOv5jwBkpZUvTqni+uGBA=="))) 15 | (testing "evaluates to the correct API signature as a base64 string" 16 | (ok (string= (cl-kraken/src/cryptography:signature path nonce data secret) 17 | expected))))) 18 | 19 | (deftest message 20 | (let* ((path "/0/private/Balance") 21 | (nonce "1234567890123456789") 22 | (data `(("pair" . "xbteur, xbtusd") ("nonce" . ,nonce)))) 23 | (testing "evaluates to the expected message in octets" 24 | (ok (equalp (cl-kraken/src/cryptography::message path nonce data) 25 | #(47 48 47 112 114 105 118 97 116 101 47 66 97 108 97 110 99 26 | 101 25 252 14 179 229 144 89 79 212 89 215 2 55 106 12 69 27 | 231 154 3 178 94 77 47 47 98 142 188 157 153 152 209 40)))))) 28 | 29 | (deftest hmac-sha512 30 | (let ((key (crypto:ascii-string-to-byte-array "abc")) 31 | (secret (crypto:ascii-string-to-byte-array "123"))) 32 | (testing "evaluates to the correct HMAC SHA512 as an array of byte octets" 33 | (ok (equalp (cl-kraken/src/cryptography::hmac-sha512 key secret) 34 | #(88 88 90 205 103 48 103 249 107 234 50 161 197 123 243 252 35 | 63 213 164 38 120 86 126 114 213 203 10 183 240 142 164 29 36 | 207 58 65 175 150 197 57 72 225 49 132 174 111 230 205 11 37 | 139 65 147 252 89 61 251 38 147 176 12 43 14 231 163 22)))))) 38 | 39 | (deftest hash-sha256 40 | (let* ((message "The quick brown fox jumped over the lazy dog's back") 41 | (octets (crypto:ascii-string-to-byte-array message ))) 42 | (testing "evaluates to the correct SHA256 hash as an array of byte octets" 43 | (ok (equalp (cl-kraken/src/cryptography::hash-sha256 octets) 44 | #(98 193 186 97 124 227 44 232 54 149 58 186 90 90 187 203 43 45 | 127 181 191 32 65 254 82 46 35 58 117 235 3 109 143)))))) 46 | -------------------------------------------------------------------------------- /tests/assets.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/assets.lisp 2 | 3 | (defpackage #:cl-kraken/tests/assets 4 | (:use #:cl #:cl-kraken #:rove) 5 | (:import-from #:cl-kraken/tests/kraken-public-data 6 | #:*all-assets* 7 | #:*bitcoin-asset* 8 | #:*usd-and-euro-assets* 9 | #:*raw-assets*)) 10 | (in-package #:cl-kraken/tests/assets) 11 | 12 | (deftest assets 13 | (testing "with no argument passed, evaluates to all assets" 14 | (ok (equal (cl-kraken:assets) *all-assets*))) 15 | ;; Test ASSET parameter. 16 | (testing "when passed \"XBT\", evaluates to Bitcoin (XBT) asset" 17 | (ok (equal (cl-kraken:assets :asset "XBT") *bitcoin-asset*))) 18 | (testing "when passed \"xbt\", evaluates to Bitcoin (XBT) asset" 19 | (ok (equal (cl-kraken:assets :asset "xbt") *bitcoin-asset*))) 20 | (testing "when passed \"usd,EUR\", evaluates to USD and Euro assets" 21 | (ok (equal (cl-kraken:assets :asset "usd,EUR") *usd-and-euro-assets*))) 22 | (testing "when passed \"UsD, euR\", evaluates to USD and Euro assets" 23 | (ok (equal (cl-kraken:assets :asset "UsD, euR") *usd-and-euro-assets*))) 24 | (testing "when passed an invalid ASSET, evaluates to unknown asset error" 25 | (ok (equal (cl-kraken:assets :asset "abc") 26 | '(:OBJ ("error" "EQuery:Unknown asset"))))) 27 | (testing "when passed an empty ASSET, evaluates to unknown asset error" 28 | (ok (equal (cl-kraken:assets :asset "") 29 | '(:OBJ ("error" "EQuery:Unknown asset"))))) 30 | (testing "when passed a symbol ASSET, a type error is signaled" 31 | (ok (signals (cl-kraken:assets :asset 'xbt) 'type-error) 32 | "The value of ASSET is XBT, which is not of type (OR STRING NULL).")) 33 | (testing "when passed a keyword ASSET, a type error is signaled" 34 | (ok (signals (cl-kraken:assets :asset :xbt) 'type-error) 35 | "The value of ASSET is :XBT, which is not of type (OR STRING NULL).")) 36 | ;; Test RAW parameter. 37 | (testing "when passed RAW T, evaluates to the raw response string" 38 | (let ((response (cl-kraken:assets :asset "xbt,usd,eur" :raw t))) 39 | (ok (stringp response)) 40 | (ok (string= response *raw-assets*)))) 41 | (testing "when passed RAW NIL, evaluates as if no RAW argument was passed" 42 | (ok (equal (cl-kraken:assets :asset "xbt" :raw nil) *bitcoin-asset*))) 43 | ;; Test invalid RAW values. 44 | (testing "when passed a string RAW, a type error is signaled" 45 | (ok (signals (cl-kraken:assets :raw "1") 'type-error) 46 | "The value of RAW is \"1\", which is not of type (MEMBER T NIL).")) 47 | (testing "when passed a symbol RAW, a type error is signaled" 48 | (ok (signals (cl-kraken:assets :raw 'a) 'type-error) 49 | "The value of RAW is 'a, which is not of type (MEMBER T NIL).")) 50 | (testing "when passed a keyword RAW, a type error is signaled" 51 | (ok (signals (cl-kraken:assets :raw :1) 'type-error) 52 | "The value of RAW is :|1|, which is not of type (MEMBER T NIL)."))) 53 | -------------------------------------------------------------------------------- /src/http.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/src/http.lisp 2 | 3 | (in-package #:cl-user) 4 | (defpackage #:cl-kraken/src/http 5 | (:documentation "HTTP request functions.") 6 | (:use #:cl) 7 | (:shadowing-import-from #:dexador 8 | #:get 9 | #:post) 10 | (:import-from #:jsown 11 | #:parse) 12 | (:import-from #:quri 13 | #:make-uri) 14 | (:import-from #:cl-kraken/src/globals 15 | +api-scheme+ 16 | +api-host+ 17 | +api-public-path+ 18 | +api-private-path+ 19 | *api-key* 20 | *api-secret*) 21 | (:import-from #:cl-kraken/src/cryptography 22 | #:signature) 23 | (:import-from #:cl-kraken/src/time 24 | #:generate-kraken-nonce) 25 | (:export #:request)) 26 | (in-package #:cl-kraken/src/http) 27 | 28 | (defun request (method &key params post raw verbose) 29 | "General HTTP GET/POST request and JSON parsing function." 30 | (check-type method (and string (not null))) 31 | (check-type params list) 32 | (check-type post boolean) 33 | (check-type raw boolean) 34 | (check-type verbose boolean) 35 | (let* ((function (if post 'post-private 'get-public)) 36 | (response (funcall function method :params params :verbose verbose))) 37 | (if raw response (parse response)))) 38 | 39 | (defun get-public (method &key params verbose 40 | (scheme +api-scheme+) (host +api-host+)) 41 | "HTTP GET request for public API queries." 42 | (check-type scheme (and string (not null))) 43 | (check-type host (and string (not null))) 44 | (let* ((path (concatenate 'string +api-public-path+ method)) 45 | (uri (make-uri :scheme scheme :host host :path path :query params))) 46 | (get uri :verbose verbose))) 47 | 48 | (defun post-private (method &key params verbose 49 | (scheme +api-scheme+) (host +api-host+) 50 | (key *api-key*) (secret *api-secret*)) 51 | "HTTP POST request for private authenticated API queries." 52 | (check-type scheme (and string (not null))) 53 | (check-type host (and string (not null))) 54 | (check-type key (and string (not null))) 55 | (check-type secret (and string (not null))) 56 | (let* ((path (concatenate 'string +api-private-path+ method)) 57 | (uri (make-uri :scheme scheme :host host :path path)) 58 | (nonce (generate-kraken-nonce)) 59 | (data (acons "nonce" nonce params)) 60 | (headers (post-http-headers path nonce data key secret))) 61 | (post uri :headers headers :content data :verbose verbose))) 62 | 63 | (defun post-http-headers (path nonce data key secret) 64 | "Kraken POST HTTP headers must contain the API key and signature." 65 | (check-type path (and string (not null))) 66 | (check-type nonce (and string (not null))) 67 | (check-type data (cons)) 68 | `(("api-key" . ,key) ("api-sign" . ,(signature path nonce data secret)))) 69 | -------------------------------------------------------------------------------- /src/time.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/src/time.lisp 2 | 3 | (in-package #:cl-user) 4 | (defpackage #:cl-kraken/src/time 5 | (:documentation "CL-Kraken time utilities.") 6 | (:use #:cl) 7 | #-(or sbcl (and ccl (not windows)) clisp ecl) 8 | (:import-from #:local-time 9 | #:now 10 | #:nsec-of 11 | #:timestamp-to-unix) 12 | #+ecl 13 | (:import-from #:cffi 14 | #:defcfun 15 | #:defcstruct 16 | #:defctype 17 | #:foreign-type-size 18 | #:mem-ref 19 | #:null-pointer 20 | #:with-foreign-object) 21 | (:export #:unix-time-in-microseconds 22 | #:generate-kraken-nonce)) 23 | (in-package #:cl-kraken/src/time) 24 | 25 | (defparameter +one-million+ 1000000) 26 | (defparameter +one-thousand+ 1000) 27 | 28 | (defun generate-kraken-nonce () 29 | "Kraken requires the nonce to be an always-increasing unsigned integer 30 | between 51 and 64 bits in length. For this, we use UNIX-TIME-IN-MICROSECONDS 31 | below, expressed as a string. This is analogous to the nonce implementations 32 | in the various other Kraken API libraries in C, C++, Go, Python, and Ruby." 33 | (princ-to-string (unix-time-in-microseconds))) 34 | 35 | #-(or sbcl (and ccl (not windows)) clisp ecl) 36 | (defun unix-time-in-microseconds (&aux (current-time (now))) 37 | "Unix Time in usec using the LOCAL-TIME library." 38 | (+ (floor (nsec-of current-time) +one-thousand+) 39 | (* +one-million+ (timestamp-to-unix current-time)))) 40 | 41 | #+sbcl 42 | (defun unix-time-in-microseconds () 43 | "Unix Time in usec using SBCL's SB-EXT:GET-TIME-OF-DAY." 44 | (multiple-value-bind (sec usec) (sb-ext:get-time-of-day) 45 | (+ (* +one-million+ sec) usec))) 46 | 47 | #+(and ccl (not windows)) 48 | (defun unix-time-in-microseconds () 49 | "Unix Time in usec using CCL external call to `gettimeofday'." 50 | (ccl:rlet ((tv :timeval)) 51 | (let ((err (ccl:external-call "gettimeofday" :address tv :address (ccl:%null-ptr) :int))) 52 | (assert (zerop err) nil "gettimeofday failed") 53 | (+ (* +one-million+ (ccl:pref tv :timeval.tv_sec)) (ccl:pref tv :timeval.tv_usec))))) 54 | 55 | #+clisp 56 | (defun unix-time-in-microseconds () 57 | "Unix Time in usec using CLISP's GET-INTERNAL-REAL-TIME. This is necessary 58 | because in CLISP, LOCAL-TIME:NOW returns precision in sec instead of usec." 59 | (get-internal-real-time)) 60 | 61 | #+ecl 62 | (progn 63 | (defctype time_t :long) 64 | (defctype seconds_t :int) 65 | 66 | (defcstruct timeval 67 | (tv_sec time_t) 68 | (tv_usec seconds_t)) 69 | 70 | (defcfun gettimeofday :int 71 | (timeval :pointer) 72 | (pointer :pointer)) 73 | 74 | (defun unix-time-in-microseconds () 75 | "Unix Time in usec using CFFI to call `gettimeofday' in C." 76 | (with-foreign-object (tv '(:struct timeval)) 77 | (gettimeofday tv (null-pointer)) 78 | (+ (* +one-million+ (mem-ref tv 'time_t)) 79 | (mem-ref tv 'seconds_t (foreign-type-size 'time_t)))))) 80 | -------------------------------------------------------------------------------- /tests/asset-pairs.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/asset-pairs.lisp 2 | 3 | (defpackage #:cl-kraken/tests/asset-pairs 4 | (:use #:cl #:cl-kraken #:rove) 5 | (:import-from #:cl-kraken/tests/kraken-public-data 6 | #:*all-pairs* 7 | #:*xbteur-pair* 8 | #:*xbtusd-and-xmreur-pairs* 9 | #:*raw-pairs*)) 10 | (in-package #:cl-kraken/tests/asset-pairs) 11 | 12 | (deftest asset-pairs 13 | (testing "with no argument passed, evaluates to all asset pairs" 14 | (ok (equal (cl-kraken:asset-pairs) *all-pairs*))) 15 | ;; Test PAIR parameter. 16 | (testing "when passed \"XBTEUR\", evaluates to XBTEUR pair" 17 | (ok (equal (cl-kraken:asset-pairs :pair "XBTEUR") *xbteur-pair*))) 18 | (testing "when passed \"xbteur\", evaluates to XBTEUR pair" 19 | (ok (equal (cl-kraken:asset-pairs :pair "xbteur") *xbteur-pair*))) 20 | (testing "when passed \"xbtusd,xmreur\", evaluates to XBTUSD+XMREUR pairs" 21 | (ok (equal (cl-kraken:asset-pairs :pair "xbtusd,xmreur") 22 | *xbtusd-and-xmreur-pairs*))) 23 | (testing "when passed \" XBTusd , xmrEUR \" evaluates to XBTUSD+XMREUR pairs" 24 | (ok (equal (cl-kraken:asset-pairs :pair " XBTusd , xmrEUR ") 25 | *xbtusd-and-xmreur-pairs*))) 26 | (testing "when passed an invalid PAIR, evaluates to unknown asset pair error" 27 | (ok (equal (cl-kraken:asset-pairs :pair "abc") 28 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 29 | (testing "when passed an empty PAIR, evaluates to unknown asset pair error" 30 | (ok (equal (cl-kraken:asset-pairs :pair "") 31 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 32 | (testing "when passed a symbol PAIR, a type error is signaled" 33 | (ok (signals (cl-kraken:asset-pairs :pair 'xbtusd) 'type-error) 34 | "The value of PAIR is XBTUSD, which is not of type (OR STRING NULL).")) 35 | (testing "when passed a keyword PAIR, a type error is signaled" 36 | (ok (signals (cl-kraken:asset-pairs :pair :xbtusd) 'type-error) 37 | "The value of PAIR is :XBTUSD, which is not of type (OR STRING NULL).")) 38 | ;; Test RAW parameter. 39 | (testing "when passed RAW T, evaluates to the raw response string" 40 | (let ((response (cl-kraken:asset-pairs :pair "xbtusd, xbteur" :raw t))) 41 | (ok (stringp response)) 42 | (ok (string= response *raw-pairs*)))) 43 | (testing "when passed RAW NIL, evaluates as if no RAW argument was passed" 44 | (ok (equal (cl-kraken:asset-pairs :pair "xbteur" :raw nil) *xbteur-pair*))) 45 | ;; Test invalid RAW values. 46 | (testing "when passed a string RAW, a type error is signaled" 47 | (ok (signals (cl-kraken:asset-pairs :raw "1") 'type-error) 48 | "The value of RAW is \"1\", which is not of type (MEMBER T NIL).")) 49 | (testing "when passed a symbol RAW, a type error is signaled" 50 | (ok (signals (cl-kraken:asset-pairs :raw 'a) 'type-error) 51 | "The value of RAW is 'a, which is not of type (MEMBER T NIL).")) 52 | (testing "when passed a keyword RAW, a type error is signaled" 53 | (ok (signals (cl-kraken:asset-pairs :raw :1) 'type-error) 54 | "The value of RAW is :|1|, which is not of type (MEMBER T NIL)."))) 55 | -------------------------------------------------------------------------------- /tests/server-time.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/server-time.lisp 2 | 3 | (defpackage #:cl-kraken/tests/server-time 4 | (:use #:cl #:cl-kraken #:rove) 5 | (:import-from #:local-time 6 | #:now 7 | #:timestamp-to-unix 8 | #:unix-to-timestamp 9 | #:format-timestring 10 | #:+utc-zone+) 11 | (:import-from #:jsown 12 | #:parse 13 | #:filter)) 14 | (in-package #:cl-kraken/tests/server-time) 15 | 16 | (defparameter *kraken-rfc1123* 17 | '(:short-weekday ", " (:day 2 #\space) #\space :short-month #\space 18 | :short-year #\space (:hour 2) #\: (:min 2) #\: (:sec 2) #\space 19 | :gmt-offset-hhmm) 20 | "Define a custom RFC1123 time format because Kraken sends a 2-digit year 21 | instead of 4 digits and a day padded with #\SPACE rather than #\0.") 22 | 23 | (defun unix-to-rfc1123 (unix-time) 24 | "Converts integer Unix Time to a string RFC1123 format timestamp." 25 | (format-timestring nil (unix-to-timestamp unix-time) 26 | :format *kraken-rfc1123* 27 | :timezone +utc-zone+)) 28 | 29 | (defun expected-list (time) 30 | "Builds the expected response list from an integer Unix Time." 31 | `(:OBJ ("error") ("result" :OBJ 32 | ("unixtime" . ,time) 33 | ("rfc1123" . ,(unix-to-rfc1123 time))))) 34 | 35 | (defun expected-string (time) 36 | "Builds the expected response string from an integer Unix Time." 37 | (concatenate 'string 38 | "{\"error\":[],\"result\":{\"unixtime\":" (princ-to-string time) 39 | ",\"rfc1123\":\"" (unix-to-rfc1123 time) "\"}}")) 40 | 41 | (deftest server-time 42 | (testing "evaluates to the expected server time" 43 | (let* ((now (timestamp-to-unix (now))) 44 | (response (cl-kraken:server-time)) 45 | (time (filter response "result" "unixtime"))) 46 | (ok (consp response)) 47 | (ok (consp (cadr (cdaddr response)))) 48 | (ok (consp (caddr (cdaddr response)))) 49 | (ok (equal response (expected-list time))) 50 | ;; evaluates to a Unix Time component expressed as an integer" 51 | (ok (integerp time)) 52 | ;; evaluates to Unix Time ±20 seconds of current time, given skew 53 | (ok (< (abs (- time now)) 20)))) 54 | ;; Test with parameter RAW NIL 55 | (let* ((response (cl-kraken:server-time :raw nil)) 56 | (time (filter response "result" "unixtime"))) 57 | (testing "when passed RAW NIL, evaluates as if no RAW parameter was passed" 58 | (ok (consp response)) 59 | (ok (consp (cadr (cdaddr response)))) 60 | (ok (consp (caddr (cdaddr response)))) 61 | (ok (equal response (expected-list time))))) 62 | ;; Test with parameter RAW T 63 | (let* ((response (cl-kraken:server-time :raw t)) 64 | (time (filter (parse response) "result" "unixtime"))) 65 | (testing "when passed RAW T, evaluates to the raw response string" 66 | (ok (simple-string-p response)) 67 | (ok (string-equal response (expected-string time))))) 68 | ;; Test invalid RAW values. 69 | (testing "when passed a string RAW, a type error is signaled" 70 | (ok (signals (cl-kraken:server-time :raw "1") 'type-error) 71 | "The value of RAW is \"1\", which is not of type (MEMBER T NIL).")) 72 | (testing "when passed a symbol RAW, a type error is signaled" 73 | (ok (signals (cl-kraken:server-time :raw 'a) 'type-error) 74 | "The value of RAW is 'a, which is not of type (MEMBER T NIL).")) 75 | (testing "when passed a keyword RAW, a type error is signaled" 76 | (ok (signals (cl-kraken:server-time :raw :1) 'type-error) 77 | "The value of RAW is :|1|, which is not of type (MEMBER T NIL)."))) 78 | -------------------------------------------------------------------------------- /tests/http.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/http.lisp 2 | 3 | (defpackage #:cl-kraken/tests/http 4 | (:use #:cl #:cl-kraken #:rove)) 5 | (in-package #:cl-kraken/tests/http) 6 | 7 | (defparameter *expected-query* "GET /0/public/Time HTTP/1.1") 8 | 9 | (deftest request 10 | (testing "when passed no keyword params" 11 | (let* ((response (cl-kraken::request "Time")) 12 | (headers (with-output-to-string (*standard-output*) response))) 13 | (ok (consp response)) 14 | (ok (string= headers "")))) 15 | (testing "when passed RAW NIL" 16 | (let* ((response (cl-kraken::request "Time" :raw nil)) 17 | (headers (with-output-to-string (*standard-output*) response))) 18 | (ok (consp response)) 19 | (ok (string= headers "")))) 20 | (testing "when passed VERBOSE NIL" 21 | (let* ((response (cl-kraken::request "Time" :verbose nil)) 22 | (headers (with-output-to-string (*standard-output*) response))) 23 | (ok (consp response)) 24 | (ok (string= headers "")))) 25 | (testing "when passed RAW NIL VERBOSE NIL" 26 | (let* ((response (cl-kraken::request "Time" :raw nil :verbose nil)) 27 | (headers (with-output-to-string (*standard-output*) response))) 28 | (ok (consp response)) 29 | (ok (string= headers "")))) 30 | (testing "when passed RAW T" 31 | (let* ((response (cl-kraken::request "Time" :raw t)) 32 | (headers (with-output-to-string (*standard-output*) response))) 33 | (ok (simple-string-p response)) 34 | (ok (string= headers "")))) 35 | (testing "when passed RAW T VERBOSE NIL" 36 | (let* ((response (cl-kraken::request "Time" :raw t :verbose nil)) 37 | (headers (with-output-to-string (*standard-output*) response))) 38 | (ok (simple-string-p response)) 39 | (ok (string= headers "")))) 40 | (testing "when passed VERBOSE T" 41 | (let ((headers (with-output-to-string (*standard-output*) 42 | (cl-kraken::request "Time" :verbose t)))) 43 | (ok (string= headers *expected-query* :start1 51 :end1 78)))) 44 | (testing "when passed RAW NIL VERBOSE T" 45 | (let ((headers (with-output-to-string (*standard-output*) 46 | (cl-kraken::request "Time" :raw nil :verbose t)))) 47 | (ok (string= headers *expected-query* :start1 51 :end1 78)))) 48 | (testing "when passed RAW T VERBOSE T" 49 | (let ((headers (with-output-to-string (*standard-output*) 50 | (cl-kraken::request "Time" :raw t :verbose t)))) 51 | (ok (string= headers *expected-query* :start1 51 :end1 78))))) 52 | 53 | (deftest post-http-headers 54 | (testing "with nonce data, evaluates to the correct headers alist" 55 | (let* ((path "/0/private/Balance") 56 | (nonce "1234567890123456789") 57 | (key "01dB/y38ooyXBUWpS7XUNguXCk1trgN/LEj7FF8LgHmk3fcvX4dNQIFD") 58 | (secret (concatenate 'string 59 | "YS/EXE3mfINjlKeegUVPT0uDUYkUX2Ed0OZp9dzCe1LO" 60 | "s+d9vZErAQKMY9o7WVQlTpvDodSlOONkZK7rngdJNw==")) 61 | (data `(("nonce" . ,nonce))) 62 | (api-sign (concatenate 63 | 'string 64 | "kc0yOGvxuk+LzgTXuvPp3Cs6BvkVhGaGZUNkatqtX2iCb30" 65 | "znwbuVX8JJYdwCisyG/7mScSYl7nZ7ihzvMXrXA=="))) 66 | (ok (equalp 67 | (cl-kraken/src/http::post-http-headers path nonce data key secret) 68 | `(("api-key" . ,key) ("api-sign" . ,api-sign)))))) 69 | (testing "with nonce + params data, evaluates to the correct headers alist" 70 | (let* ((path "/0/private/Balance") 71 | (nonce "1234567890123456789") 72 | (key "01dB/y38ooyXBUWpS7XUNguXCk1trgN/LEj7FF8LgHmk3fcvX4dNQIFD") 73 | (secret (concatenate 'string 74 | "YS/EXE3mfINjlKeegUVPT0uDUYkUX2Ed0OZp9dzCe1LO" 75 | "s+d9vZErAQKMY9o7WVQlTpvDodSlOONkZK7rngdJNw==")) 76 | (data `(("pair" . "xbteur, xbtusd") ("nonce" . ,nonce))) 77 | (api-sign (concatenate 78 | 'string 79 | "lQjzgTnvmjJ9HMiucF+M3T7cI/VTYjZFptWDbf0uFG6RXLH" 80 | "sedsZaJ8n+HPn8G5exNwkzQC3phqXRqUi7g96Gw=="))) 81 | (ok (equalp 82 | (cl-kraken/src/http::post-http-headers path nonce data key secret) 83 | `(("api-key" . ,key) ("api-sign" . ,api-sign))))))) 84 | -------------------------------------------------------------------------------- /tests/ticker.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/ticker.lisp 2 | 3 | (defpackage #:cl-kraken/tests/ticker 4 | (:use #:cl #:cl-kraken #:rove) 5 | (:import-from #:jsown 6 | #:filter)) 7 | (in-package #:cl-kraken/tests/ticker) 8 | 9 | (deftest ticker 10 | (testing "when passed \"xBteuR\", evaluates to XBTEUR ticker" 11 | (let* ((response (cl-kraken:ticker "xBteuR")) 12 | (error! (filter response "error")) 13 | (result (filter response "result")) 14 | (pair (filter result "XXBTZEUR")) 15 | (ask (filter pair "a")) 16 | (bid (filter pair "b")) 17 | (closed (filter pair "c")) 18 | (volume (filter pair "v")) 19 | (vwap (filter pair "p")) 20 | (number (filter pair "t")) 21 | (low (filter pair "l")) 22 | (high (filter pair "h")) 23 | (open (filter pair "o"))) 24 | (ok (consp response)) 25 | (ok (= (length response) 3)) 26 | (ok (eq (first response) :OBJ)) 27 | (ok (equal (second response) '("error"))) 28 | (ok (null error!)) 29 | (ok (consp result)) 30 | (ok (= (length result) 2)) 31 | (ok (consp pair)) 32 | (ok (= (length pair) 10)) 33 | (ok (consp ask)) 34 | (ok (= (length ask) 3)) 35 | (ok (consp bid)) 36 | (ok (= (length bid) 3)) 37 | (ok (consp closed)) 38 | (ok (= (length closed) 2)) 39 | (ok (consp volume)) 40 | (ok (= (length volume) 2)) 41 | (ok (consp vwap)) 42 | (ok (= (length vwap) 2)) 43 | (ok (consp number)) 44 | (ok (= (length number) 2)) 45 | (ok (consp low)) 46 | (ok (= (length low) 2)) 47 | (ok (consp high)) 48 | (ok (stringp open)) 49 | (ok (= (length high) 2)))) 50 | (testing "when passed \" xbtjpy ,ETCusd \", evaluates to XBTJPY+ETCUSD ticker" 51 | (let* ((response (cl-kraken:ticker " xbtjpy ,ETCusd ")) 52 | (error! (filter response "error")) 53 | (result (filter response "result")) 54 | (pair-1 (filter result "XXBTZJPY")) 55 | (pair-2 (filter result "XETCZUSD"))) 56 | (ok (null error!)) 57 | (ok (consp result)) 58 | (ok (= (length result) 3)) 59 | (ok (consp pair-1)) 60 | (ok (= (length pair-1) 10)) 61 | (ok (consp pair-2)) 62 | (ok (= (length pair-2) 10)))) 63 | (testing "when passed an invalid PAIR, evaluates to unknown asset pair error" 64 | (ok (equal (cl-kraken:ticker "abc") 65 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 66 | (testing "when passed an empty PAIR, evaluates to unknown asset pair error" 67 | (ok (equal (cl-kraken:ticker "") 68 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 69 | (testing "when passed a symbol PAIR, a type error is signaled" 70 | (ok (signals (cl-kraken:ticker 'xbteur) 'type-error) 71 | "The value of PAIR is XBTEUR, which is not of type (AND STRING (NOT NULL)).")) 72 | (testing "when passed a keyword PAIR, a type error is signaled" 73 | (ok (signals (cl-kraken:ticker :xbteur) 'type-error) 74 | "The value of PAIR is :XBTEUR, which is not of type (AND STRING (NOT NULL)).")) 75 | ;; Test RAW parameter. 76 | (testing "when passed RAW T, evaluates to the raw response string" 77 | (let ((response (cl-kraken:ticker "xbteur" :raw t)) 78 | (expected "{\"error\":[],\"result\":{\"XXBTZEUR\":{\"a\":[")) 79 | (ok (stringp response)) 80 | (ok (string= response expected :start1 0 :end1 39)))) 81 | (testing "when passed RAW NIL, evaluates as if no RAW argument was passed" 82 | (let* ((response (cl-kraken:ticker "xbtusd" :raw nil)) 83 | (error! (filter response "error")) 84 | (result (filter response "result")) 85 | (pair (filter result "XXBTZUSD"))) 86 | (ok (consp response)) 87 | (ok (= (length response) 3)) 88 | (ok (eq (first response) :OBJ)) 89 | (ok (equal (second response) '("error"))) 90 | (ok (null error!)) 91 | (ok (consp result)) 92 | (ok (= (length result) 2)) 93 | (ok (consp pair)) 94 | (ok (= (length pair) 10)))) 95 | ;; Test invalid RAW values. 96 | (testing "when passed a string RAW, a type error is signaled" 97 | (ok (signals (cl-kraken:ticker "xbteur" :raw "1") 'type-error) 98 | "The value of RAW is \"1\", which is not of type (MEMBER T NIL).")) 99 | (testing "when passed a symbol RAW, a type error is signaled" 100 | (ok (signals (cl-kraken:ticker "xbteur" :raw 'a) 'type-error) 101 | "The value of RAW is 'a, which is not of type (MEMBER T NIL).")) 102 | (testing "when passed a keyword RAW, a type error is signaled" 103 | (ok (signals (cl-kraken:ticker "xbteur" :raw :1) 'type-error) 104 | "The value of RAW is :|1|, which is not of type (MEMBER T NIL)."))) 105 | -------------------------------------------------------------------------------- /tests/spread.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/spread.lisp 2 | 3 | (defpackage #:cl-kraken/tests/spread 4 | (:use #:cl #:cl-kraken #:rove) 5 | (:import-from #:local-time 6 | #:now 7 | #:timestamp-to-unix) 8 | (:import-from #:jsown 9 | #:filter) 10 | (:import-from #:parse-float 11 | #:parse-float)) 12 | (in-package #:cl-kraken/tests/spread) 13 | 14 | (deftest spread 15 | (let ((unix-now (timestamp-to-unix (now)))) 16 | (testing "when passed \"xbteur\", evaluates to XBTEUR spread data" 17 | (let* ((response (cl-kraken:spread "xbteur" :since unix-now)) 18 | (error! (filter response "error")) 19 | (result (filter response "result")) 20 | (pair (filter result "XXBTZEUR")) 21 | (last (filter result "last"))) 22 | (ok (consp response)) 23 | (ok (= (length response) 3)) 24 | (ok (eq (first response) :OBJ)) 25 | (ok (equal (second response) '("error"))) 26 | (ok (null error!)) 27 | (ok (consp result)) 28 | (ok (= (length result) 3)) 29 | (ok (listp pair)) 30 | (ok (integerp last)) 31 | (ok (= (integer-length last) 31)))) 32 | ;; Test invalid PAIR values. 33 | (testing "when passed a multiple PAIR, evaluates to unknown asset pair error" 34 | (ok (equal (cl-kraken:spread "xbteur,xbtusd") 35 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 36 | (testing "when passed an invalid PAIR, evaluates to unknown asset pair error" 37 | (ok (equal (cl-kraken:spread "abc") 38 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 39 | (testing "when passed an empty PAIR, evaluates to invalid arguments error" 40 | (ok (equal (cl-kraken:spread "") 41 | '(:OBJ ("error" "EGeneral:Invalid arguments"))))) 42 | (testing "when passed a symbol PAIR, a type error is signaled" 43 | (ok (signals (cl-kraken:spread 'xbteur) 'type-error) 44 | "The value of PAIR is XBTEUR, which is not of type SIMPLE-STRING.")) 45 | (testing "when passed a keyword PAIR, a type error is signaled" 46 | (ok (signals (cl-kraken:spread :xbteur) 'type-error) 47 | "The value of PAIR is :XBTEUR, which is not of type SIMPLE-STRING.")) 48 | ;; Test correct handling of SINCE keyword parameter to query params. 49 | (testing "when no SINCE is passed, it is absent from the query params" 50 | (let ((headers (with-output-to-string (*standard-output*) 51 | (cl-kraken:spread "xbteur" :verbose t)))) 52 | (ok (string= headers "Spread?pair=xbteur " :start1 65 :end1 84)))) 53 | (testing "when passed a valid SINCE, it is present in the query params" 54 | (let* ((since (princ-to-string unix-now)) 55 | (headers (with-output-to-string (*standard-output*) 56 | (cl-kraken:spread "xbteur" :since unix-now :verbose t))) 57 | (expected (concatenate 'string "Spread?since=" since "&pair=xbteur"))) 58 | (ok (string= headers expected :start1 65 :end1 (+ 90 (length since)))))) 59 | ;; Test invalid SINCE values. 60 | (testing "when passed a string SINCE, a type error is signaled" 61 | (ok (signals (cl-kraken:spread "xbteur" :since "1") 'type-error) 62 | "The value of SINCE is \"1\", which is not of type INTEGER.")) 63 | (testing "when passed a symbol SINCE, a type error is signaled" 64 | (ok (signals (cl-kraken:spread "xbteur" :since 'a) 'type-error) 65 | "The value of SINCE is 'a, which is not of type INTEGER.")) 66 | (testing "when passed a keyword SINCE, a type error is signaled" 67 | (ok (signals (cl-kraken:spread "xbteur" :since :1) 'type-error) 68 | "The value of SINCE is :|1|, which is not of type INTEGER."))) 69 | ;; Test RAW parameter. 70 | (testing "when passed RAW T, evaluates to the raw response string" 71 | (let* ((unix-now (timestamp-to-unix (now))) 72 | (response (cl-kraken:spread "xbtusd" :since unix-now :raw t)) 73 | (expected "{\"error\":[],\"result\":{\"XXBTZUSD\":[")) 74 | (ok (stringp response)) 75 | (ok (string= response expected :start1 0 :end1 34)))) 76 | (testing "when passed RAW NIL, evaluates as if no RAW argument was passed" 77 | (let* ((unix-now (timestamp-to-unix (now))) 78 | (response (cl-kraken:spread "xbteur" :since unix-now :raw nil)) 79 | (error! (filter response "error")) 80 | (result (filter response "result"))) 81 | (ok (consp response)) 82 | (ok (= (length response) 3)) 83 | (ok (eq (first response) :OBJ)) 84 | (ok (equal (second response) '("error"))) 85 | (ok (null error!)) 86 | (ok (consp result)))) 87 | ;; Test invalid RAW values. 88 | (testing "when passed a string RAW, a type error is signaled" 89 | (ok (signals (cl-kraken:spread "xbteur" :raw "1") 'type-error) 90 | "The value of RAW is \"1\", which is not of type (MEMBER T NIL).")) 91 | (testing "when passed a symbol RAW, a type error is signaled" 92 | (ok (signals (cl-kraken:spread "xbteur" :raw 'a) 'type-error) 93 | "The value of RAW is 'a, which is not of type (MEMBER T NIL).")) 94 | (testing "when passed a keyword RAW, a type error is signaled" 95 | (ok (signals (cl-kraken:spread "xbteur" :raw :1) 'type-error) 96 | "The value of RAW is :|1|, which is not of type (MEMBER T NIL)."))) 97 | -------------------------------------------------------------------------------- /tests/trade-volume.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/trade-volume.lisp 2 | 3 | (defpackage #:cl-kraken/tests/trade-volume 4 | (:use #:cl #:cl-kraken #:rove)) 5 | (in-package #:cl-kraken/tests/trade-volume) 6 | 7 | (defparameter *default-response-object* 8 | `(:OBJ ("error") ("result" :OBJ ("currency" . "ZUSD") ("volume" . "0.0000"))) 9 | "Expected default response object") 10 | 11 | (defparameter *pair-response-object* 12 | `(:OBJ ("error") 13 | ("result" :OBJ 14 | ("currency" . "ZUSD") 15 | ("volume" . "0.0000") 16 | ("fees" :OBJ ("XXBTZEUR" :OBJ 17 | ("fee" . "0.2600") 18 | ("minfee" . "0.1000") 19 | ("maxfee" . "0.2600") 20 | ("nextfee" . "0.2400") 21 | ("nextvolume" . "50000.0000") 22 | ("tiervolume" . "0.0000"))) 23 | ("fees_maker" :OBJ ("XXBTZEUR" :OBJ ("fee" . "0.1600") 24 | ("minfee" . "0.0000") 25 | ("maxfee" . "0.1600") 26 | ("nextfee" . "0.1400") 27 | ("nextvolume" . "50000.0000") 28 | ("tiervolume" . "0.0000"))))) 29 | "Expected response object when passed a PAIR value of xbteur") 30 | 31 | (defparameter *default-response-string* 32 | (concatenate 33 | 'string 34 | "{\"error\":[],\"result\":{\"currency\":\"ZUSD\",\"volume\":\"0.0000\"}}") 35 | "Expected default response object") 36 | 37 | (defparameter *pair-response-string* 38 | (concatenate 'string 39 | "{\"error\":[],\"result\":{" 40 | "\"currency\":\"ZUSD\",\"volume\":\"0.0000\",\"fees\":{" 41 | "\"XXBTZEUR\":{\"fee\":\"0.2600\",\"minfee\":\"0.1000\"," 42 | "\"maxfee\":\"0.2600\",\"nextfee\":\"0.2400\"," 43 | "\"nextvolume\":\"50000.0000\",\"tiervolume\":\"0.0000\"" 44 | "}},\"fees_maker\":{" 45 | "\"XXBTZEUR\":{\"fee\":\"0.1600\",\"minfee\":\"0.0000\"," 46 | "\"maxfee\":\"0.1600\",\"nextfee\":\"0.1400\"," 47 | "\"nextvolume\":\"50000.0000\",\"tiervolume\":\"0.0000\"}}}}") 48 | "Expected JSON raw string response when passed a PAIR value of xbteur") 49 | 50 | (defparameter *2-pair-response-string* 51 | (concatenate 'string 52 | "{\"error\":[],\"result\":" 53 | "{\"currency\":\"ZUSD\",\"volume\":\"0.0000\",\"fees\":{" 54 | "\"XXBTZUSD\":{\"fee\":\"0.2600\",\"minfee\":\"0.1000\"," 55 | "\"maxfee\":\"0.2600\",\"nextfee\":\"0.2400\"," 56 | "\"nextvolume\":\"50000.0000\",\"tiervolume\":\"0.0000\"}," 57 | "\"XXBTZEUR\":{\"fee\":\"0.2600\",\"minfee\":\"0.1000\"," 58 | "\"maxfee\":\"0.2600\",\"nextfee\":\"0.2400\"," 59 | "\"nextvolume\":\"50000.0000\",\"tiervolume\":\"0.0000\"" 60 | "}},\"fees_maker\":{" 61 | "\"XXBTZUSD\":{\"fee\":\"0.1600\",\"minfee\":\"0.0000\"," 62 | "\"maxfee\":\"0.1600\",\"nextfee\":\"0.1400\"," 63 | "\"nextvolume\":\"50000.0000\",\"tiervolume\":\"0.0000\"}," 64 | "\"XXBTZEUR\":{\"fee\":\"0.1600\",\"minfee\":\"0.0000\"," 65 | "\"maxfee\":\"0.1600\",\"nextfee\":\"0.1400\"," 66 | "\"nextvolume\":\"50000.0000\",\"tiervolume\":\"0.0000\"}}}}") 67 | "Expected JSON raw string response when passed a PAIR value of xbteur,xbtusd") 68 | 69 | (deftest trade-volume 70 | (testing "when passed no keyword params" 71 | (ok (equal (cl-kraken:trade-volume) *default-response-object*))) 72 | (testing "when passed a PAIR of NIL" 73 | (ok (equal (cl-kraken:trade-volume :pair nil) *default-response-object*))) 74 | (testing "when passed RAW T" 75 | (ok (string= (cl-kraken:trade-volume :raw t) *default-response-string*))) 76 | (testing "when passed a valid PAIR" 77 | (ok (equal (cl-kraken:trade-volume :pair "xbteur") *pair-response-object*))) 78 | (testing "when passed a valid PAIR with extra spaces and RAW T" 79 | (ok (equal (cl-kraken:trade-volume :pair " xbteur " :raw t) 80 | *pair-response-string*))) 81 | (testing "when passed 2 valid PAIRs and RAW T" 82 | (ok (equal (cl-kraken:trade-volume :pair " xbteur , xbtusd " :raw t) 83 | *2-pair-response-string*))) 84 | ;; Test invalid PAIR values. 85 | (testing "when passed an invalid PAIR, evaluates to unknown asset pair error" 86 | (ok (equal (cl-kraken:trade-volume :pair "abc") 87 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 88 | (testing "when passed an empty PAIR, evaluates to unknown asset pair error" 89 | (ok (equal (cl-kraken:trade-volume :pair "") 90 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 91 | (testing "when passed a symbol PAIR, a type error is signaled" 92 | (ok (signals (cl-kraken:trade-volume :pair 'xbteur) 'type-error) 93 | "The value of PAIR is not of type (OR SIMPLE-BASE-STRING NULL).")) 94 | (testing "when passed a keyword PAIR, a type error is signaled" 95 | (ok (signals (cl-kraken:trade-volume :pair :xbteur) 'type-error) 96 | "The value of PAIR is not of type (OR SIMPLE-BASE-STRING NULL).")) 97 | ;; Test FEE-INFO parameter which for now has no effect. 98 | (testing "when passed a valid PAIR with FEE-INFO T" 99 | (ok (equal (cl-kraken:trade-volume :pair " xbteur" :fee-info t) 100 | *pair-response-object*))) 101 | (testing "when passed a valid PAIR with FEE-INFO NIL" 102 | (ok (equal (cl-kraken:trade-volume :pair "xbteur " :fee-info nil) 103 | *pair-response-object*)))) 104 | -------------------------------------------------------------------------------- /tests/trades.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/trades.lisp 2 | 3 | (defpackage #:cl-kraken/tests/trades 4 | (:use #:cl #:cl-kraken #:rove) 5 | (:import-from #:jsown 6 | #:filter) 7 | (:import-from #:parse-float 8 | #:parse-float) 9 | (:import-from #:cl-kraken 10 | #:server-time)) 11 | (in-package #:cl-kraken/tests/trades) 12 | 13 | (deftest trades 14 | (testing "when passed \"xbtUSD\", evaluates to data on 1000 XBTUSD trades" 15 | (let* ((response (cl-kraken:trades "xbtUSD")) 16 | (error! (filter response "error")) 17 | (result (filter response "result")) 18 | (last (filter result "last")) 19 | (pair (filter result "XXBTZUSD")) 20 | (trade (first pair))) 21 | (ok (consp response)) 22 | (ok (= (length response) 3)) 23 | (ok (eq (first response) :OBJ)) 24 | (ok (equal (second response) '("error"))) 25 | (ok (null error!)) 26 | (ok (consp result)) 27 | (ok (= (length result) 3)) 28 | (ok (simple-string-p last)) 29 | (ok (= (length last) 19)) 30 | (ok (listp pair)) 31 | (ok (= (length pair) 1000)) 32 | (ok (listp trade)) 33 | (ok (= (length trade) 6)) 34 | (destructuring-bind (price volume time buy/sell market/limit misc) trade 35 | (ok (simple-string-p price)) 36 | (ok (simple-string-p volume)) 37 | (ok (floatp (parse-float price))) 38 | (ok (floatp (parse-float volume))) 39 | (ok (typep time 'ratio)) 40 | (ok (or (string= buy/sell "b") (string= buy/sell "s"))) 41 | (ok (or (string= market/limit "m") (string= market/limit "l"))) 42 | (ok (string= misc ""))))) 43 | ;; Test invalid PAIR values. 44 | (testing "when passed a multiple PAIR, evaluates to unknown asset pair error" 45 | (ok (equal (cl-kraken:trades "xbteur,xbtusd") 46 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 47 | (testing "when passed an invalid PAIR, evaluates to unknown asset pair error" 48 | (ok (equal (cl-kraken:trades "abc") 49 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 50 | (testing "when passed an empty PAIR, evaluates to invalid arguments error" 51 | (ok (equal (cl-kraken:trades "") 52 | '(:OBJ ("error" "EGeneral:Invalid arguments"))))) 53 | (testing "when passed a valid PAIR with spaces -> unknown asset pair error" 54 | (ok (equal (cl-kraken:trades " xbtusd") 55 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 56 | (testing "when passed a symbol PAIR, a type error is signaled" 57 | (ok (signals (cl-kraken:trades 'xbteur) 'type-error) 58 | "The value of PAIR is XBTEUR, which is not of type SIMPLE-STRING.")) 59 | (testing "when passed a keyword PAIR, a type error is signaled" 60 | (ok (signals (cl-kraken:trades :xbteur) 'type-error) 61 | "The value of PAIR is :XBTEUR, which is not of type SIMPLE-STRING.")) 62 | ;; Test correct handling of SINCE keyword parameter to query params. 63 | (testing "when no SINCE is passed, it is absent from the query params" 64 | (let ((headers (with-output-to-string (*standard-output*) 65 | (cl-kraken:trades "xbteur" :verbose t)))) 66 | (ok (string= headers "Trades?pair=xbteur " :start1 65 :end1 84)))) 67 | (testing "when passed an integer SINCE, it is present in the query params" 68 | (let* ((server-time (filter (server-time) "result" "unixtime")) 69 | (kraken-time (* server-time 1000 1000 1000)) 70 | (since (princ-to-string kraken-time)) 71 | (headers (with-output-to-string (*standard-output*) 72 | (cl-kraken:trades "xbteur" :since kraken-time 73 | :verbose t))) 74 | (expected (concatenate 'string 75 | "Trades?since=" since "&pair=xbteur"))) 76 | (ok (string= headers expected :start1 65 :end1 (+ 90 (length since)))))) 77 | ;; Test invalid SINCE values. 78 | (testing "when passed a string SINCE, a type error is signaled" 79 | (ok (signals (cl-kraken:trades "xbteur" :since "1") 'type-error) 80 | "The value of SINCE is \"1\", which is not of type INTEGER.")) 81 | (testing "when passed a symbol SINCE, a type error is signaled" 82 | (ok (signals (cl-kraken:trades "xbteur" :since 'a) 'type-error) 83 | "The value of SINCE is 'a, which is not of type INTEGER.")) 84 | (testing "when passed a keyword SINCE, a type error is signaled" 85 | (ok (signals (cl-kraken:trades "xbteur" :since :1) 'type-error) 86 | "The value of SINCE is :|1|, which is not of type INTEGER.")) 87 | ;; Test RAW parameter. 88 | (testing "when passed RAW T, evaluates to the raw response string" 89 | (let ((response (cl-kraken:trades "xbteur" :raw t)) 90 | (expected "{\"error\":[],\"result\":{\"XXBTZEUR\":[[")) 91 | (ok (stringp response)) 92 | (ok (string= response expected :start1 0 :end1 35 )))) 93 | (testing "when passed RAW NIL, evaluates as if no RAW argument was passed" 94 | (let* ((response (cl-kraken:trades "xbtusd" :raw nil)) 95 | (error! (filter response "error")) 96 | (result (filter response "result")) 97 | (last (filter result "last")) 98 | (pair (filter result "XXBTZUSD")) 99 | (trade (first pair))) 100 | (ok (consp response)) 101 | (ok (= (length response) 3)) 102 | (ok (eq (first response) :OBJ)) 103 | (ok (equal (second response) '("error"))) 104 | (ok (null error!)) 105 | (ok (consp result)) 106 | (ok (= (length result) 3)) 107 | (ok (simple-string-p last)) 108 | (ok (= (length last) 19)) 109 | (ok (listp pair)) 110 | (ok (= (length pair) 1000)) 111 | (ok (listp trade)) 112 | (ok (= (length trade) 6)))) 113 | ;; Test invalid RAW values. 114 | (testing "when passed a string RAW, a type error is signaled" 115 | (ok (signals (cl-kraken:trades "xbteur" :raw "1") 'type-error) 116 | "The value of RAW is \"1\", which is not of type (MEMBER T NIL).")) 117 | (testing "when passed a symbol RAW, a type error is signaled" 118 | (ok (signals (cl-kraken:trades "xbteur" :raw 'a) 'type-error) 119 | "The value of RAW is 'a, which is not of type (MEMBER T NIL).")) 120 | (testing "when passed a keyword RAW, a type error is signaled" 121 | (ok (signals (cl-kraken:trades "xbteur" :raw :1) 'type-error) 122 | "The value of RAW is :|1|, which is not of type (MEMBER T NIL)."))) 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Quicklisp](http://quickdocs.org/badge/cl-kraken.svg)](http://quickdocs.org/cl-kraken/) 2 | [![Build Status](https://travis-ci.com/jonatack/cl-kraken.svg?branch=master)](https://travis-ci.com/jonatack/cl-kraken) 3 | [![Coverage Status](https://coveralls.io/repos/github/jonatack/cl-kraken/badge.svg?branch=master)](https://coveralls.io/github/jonatack/cl-kraken?branch=master) 4 | 5 | # CL-KRAKEN 6 | 7 | CL-KRAKEN is an API wrapper for the Kraken cryptocurrency exchange written in 8 | Common Lisp. For the moment, CL-KRAKEN enables all of the public market data API 9 | in the [Kraken REST API documentation](https://www.kraken.com/features/api) and 10 | some of the private user data API. See the API section below for details. 11 | 12 | Currently a side project for learning Common Lisp: language, portability, 13 | packages, unit testing, interfacing with outside libraries and the real world, 14 | and so on. Suggestions and pull requests welcome! 15 | 16 | 17 | ## Portability 18 | 19 | Developed with SBCL (in general the latest version) and tested with: 20 | 21 | - Armed Bear Common Lisp (ABCL) 1.5.0, 1.6.0-dev, 1.7.0 22 | - CLISP 2.49.92, 2.49.93 23 | - Clozure Common Lisp (CCL) 1.11.5, 1.12-dev.5 24 | - Embeddable Common Lisp (ECL) 16.1.3, 20.4.24 25 | 26 | Allowed failures on Travis are due to CI build issues and not portability ones. 27 | 28 | ## Dependencies 29 | 30 | CL-KRAKEN imports a small number of functions from the following Common Lisp 31 | libraries: DEXADOR, JSOWN, QURI, LOCAL-TIME, IRONCLAD, CL-BASE64, and for ECL, 32 | CFFI. 33 | 34 | 35 | ## Getting started 36 | 37 | ```lisp 38 | (ql:quickload :cl-kraken) 39 | (in-package :cl-kraken) 40 | ``` 41 | 42 | 43 | ## API 44 | 45 | All API calls accept the following optional boolean keyword parameters: 46 | 47 | - RAW (T or default NIL) to return the JSON response as a raw string instead of 48 | parsed and converted to a list data structure. 49 | 50 | - VERBOSE (T or default NIL) to output the HTTP request headers for verifying 51 | and debugging. 52 | 53 | ### Public market data API calls 54 | 55 | ```lisp 56 | ;;; ASSET PAIRS 57 | ;;; Get data on one or more (or all) asset pairs tradeable on Kraken. 58 | ;;; Pairs are passed as an optional case-insensitive, comma-delimited string. 59 | (asset-pairs &key pair raw verbose) 60 | ;;; 61 | (asset-pairs) 62 | (asset-pairs :pair "XBTUSD") 63 | (asset-pairs :pair "xbteur,ethusd" :verbose t) 64 | (asset-pairs :pair "XBTUSD, xbteur, ETHJPY, ethgbp" :raw t :verbose t) 65 | 66 | ;;; ASSETS 67 | ;;; Get data on one or more (or all) assets available on Kraken. 68 | ;;; Assets are passed as an optional case-insensitive, comma-delimited string. 69 | (assets &key asset raw verbose) 70 | ;;; 71 | (assets) 72 | (assets :asset "xbt") 73 | (assets :asset "xbt,usd,eur,dash,xmr" :raw t) 74 | (assets :asset "xbt, USD, eur, JPY, eth, ZEC, ltc" :verbose t) 75 | 76 | ;;; DEPTH (Order Book) 77 | ;;; Get order book price data for an asset pair. 78 | ;;; PAIR is a required case-insensitive string representing a single asset pair 79 | ;;; for which to query depth. 80 | ;;; COUNT is an optional integer of maximum asks and bids to receive. 81 | (depth pair &key count raw verbose) 82 | ;;; 83 | (depth "xbteur") 84 | (depth "ADAXBT" :count 1 :raw t) 85 | (depth "LtcUsd" :count 10 :verbose t) 86 | 87 | ;;; OHLC 88 | ;;; Get OHLC (Open, High, Low, Close) price data for an asset pair. 89 | ;;; PAIR is a required, case-insensitive string representing a single asset pair 90 | ;;; for which to query OHLC data. 91 | ;;; INTERVAL is an optional integer time interval in minutes defaulting to 1. 92 | ;;; Permitted values are 1, 5, 15, 30, 60, 240, 1440, 10080, 21600. 93 | ;;; SINCE is an optional integer Unix Time id to specify from when to return 94 | ;;; new committed OHLC data, corresponding to previous OHLC `last' values. 95 | (ohlc pair &key since (interval 1) raw verbose) 96 | ;;; 97 | (ohlc "xbteur") 98 | (ohlc "ZECEUR" :since 1548265854 :raw t) 99 | (ohlc "EthUsd" :interval 15 :since 1548265854 :verbose t) 100 | 101 | ;;; SERVER TIME 102 | ;;; Get Kraken server time. Useful to approximate skew time between server and client. 103 | (server-time &key raw verbose) 104 | ;;; 105 | (server-time) 106 | (server-time :raw t :verbose t) 107 | 108 | ;;; SPREAD 109 | ;;; Get spread price data for an asset pair. 110 | ;;; PAIR is a required, case-insensitive string representing a single asset pair. 111 | ;;; SINCE is an optional integer Unix Time id from when to return spread data, 112 | ;;; corresponding to previous spread `last' values. 113 | (spread pair &key since raw verbose) 114 | ;;; 115 | (spread "XBTEUR") 116 | (spread "zecjpy" :since 1551009182) 117 | (spread "EthUsd" :since 1551009182 :raw t :verbose t) 118 | 119 | ;;; TICKER 120 | ;;; Get ticker data for one or more asset pairs. 121 | ;;; Pairs are passed as a required case-insensitive, comma-delimited string. 122 | (ticker pair &key raw verbose) 123 | ;;; 124 | (ticker "XBTUSD") 125 | (ticker "xbtusd,etcxbt,XBTEUR") 126 | (ticker "xbtusd, etcxbt, XBTEUR, XBTGBP" :raw t :verbose t) 127 | 128 | ;;; TRADES 129 | ;;; Get recent trades for an asset pair. 130 | ;;; PAIR is a required, case-insensitive string representing a single asset pair. 131 | ;;; SINCE is an optional integer timestamp id from when to return trades data, 132 | ;;; corresponding to previous trades `last' values. 133 | (trades pair &key since raw verbose) 134 | ;;; 135 | (trades "xbtusd") 136 | (trades "ETHGBP" :since 1551123951304758112) 137 | (trades "ltcUSD" :since 1551123951304758112 :raw t :verbose t) 138 | ``` 139 | ### Private user data API calls 140 | 141 | ```lisp 142 | ;;; TRADE-VOLUME 143 | ;;; Get account trade volume. 144 | ;;; PAIR is an optional, comma-delimited, case-insensitive asset pair string; 145 | ;;; if not provided, defaults to all pairs. 146 | ;;; FEE-INFO is an optional boolean whether or not to include fee info in the 147 | ;;; result. Note: Currently appears to be not implemented by Kraken, since 148 | ;;; the fee info is always returned. 149 | (trade-volume &key pair fee-info raw verbose) 150 | ;;; 151 | (trade-volume) 152 | (trade-volume :pair "xbtusd, xbteur") 153 | (trade-volume :pair "ltcGBP" :fee-info t :raw t :verbose t) 154 | ``` 155 | 156 | ## Tests 157 | 158 | To run the test suite, the ROVE test library needs to be loaded. 159 | 160 | ```lisp 161 | (ql:quickload :rove) 162 | ``` 163 | 164 | Then run the tests using one of the following: 165 | 166 | ```lisp 167 | (asdf:test-system :cl-kraken) ; Detailed test output. 168 | (rove:run :cl-kraken/tests :style :spec) ; Detailed test output. 169 | (rove:run :cl-kraken/tests :style :dot) ; One dot per test output (in Rove master). 170 | (rove:run :cl-kraken/tests :style :none) ; Minimal test output. 171 | ``` 172 | 173 | To run the tests of one test file only, append the file name without the extension: 174 | 175 | ```lisp 176 | (rove:run :cl-kraken/tests/cryptography) ; Run tests in tests/cryptography.lisp only. 177 | ``` 178 | 179 | ### Author 180 | 181 | * Jon Atack (jon@atack.com) 182 | 183 | 184 | ### Copyright 185 | 186 | Copyright (c) 2019-2020 Jon Atack (jon@atack.com) 187 | -------------------------------------------------------------------------------- /tests/depth.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/depth.lisp 2 | 3 | (defpackage #:cl-kraken/tests/depth 4 | (:use #:cl #:cl-kraken #:rove) 5 | (:import-from #:jsown 6 | #:filter) 7 | (:import-from #:parse-float 8 | #:parse-float)) 9 | (in-package #:cl-kraken/tests/depth) 10 | 11 | (deftest depth 12 | (testing "when passed \"xbteur\", evaluates to XBTEUR depth order book" 13 | (let* ((count 1) 14 | (response (cl-kraken:depth "xbteur" :count count)) 15 | (error! (filter response "error")) 16 | (result (filter response "result")) 17 | (pair (filter result "XXBTZEUR")) 18 | (asks (filter pair "asks")) 19 | (bids (filter pair "bids"))) 20 | (ok (consp response)) 21 | (ok (= (length response) 3)) 22 | (ok (eq (first response) :OBJ)) 23 | (ok (equal (second response) '("error"))) 24 | (ok (null error!)) 25 | (ok (consp result)) 26 | (ok (= (length result) 2)) 27 | (ok (consp pair)) 28 | (ok (= (length pair) 3)) 29 | (ok (consp asks)) 30 | (ok (= (length asks) count)) 31 | (ok (consp bids)) 32 | (ok (= (length bids) count)) 33 | (destructuring-bind (ask-price ask-volume ask-timestamp) (car asks) 34 | (ok (simple-string-p ask-price)) 35 | (ok (simple-string-p ask-volume)) 36 | (ok (floatp (parse-float ask-price))) 37 | (ok (floatp (parse-float ask-volume))) 38 | (ok (integerp ask-timestamp))) 39 | (destructuring-bind (bid-price bid-volume bid-timestamp) (car bids) 40 | (ok (simple-string-p bid-price)) 41 | (ok (simple-string-p bid-volume)) 42 | (ok (floatp (parse-float bid-price))) 43 | (ok (floatp (parse-float bid-volume))) 44 | (ok (integerp bid-timestamp))))) 45 | ;; Test invalid PAIR values. 46 | (testing "when passed a multiple PAIR, evaluates to unknown asset pair error" 47 | (ok (equal (cl-kraken:depth "xbteur,xbtusd") 48 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 49 | (testing "when passed an invalid PAIR, evaluates to unknown asset pair error" 50 | (ok (equal (cl-kraken:depth "abc") 51 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 52 | (testing "when passed an empty PAIR, evaluates to invalid arguments error" 53 | (ok (equal (cl-kraken:depth "") 54 | '(:OBJ ("error" "EGeneral:Invalid arguments"))))) 55 | (testing "when passed a symbol PAIR, a type error is signaled" 56 | (ok (signals (cl-kraken:depth 'xbteur) 'type-error) 57 | "The value of PAIR is XBTEUR, which is not of type SIMPLE-STRING.")) 58 | (testing "when passed a keyword PAIR, a type error is signaled" 59 | (ok (signals (cl-kraken:depth :xbteur) 'type-error) 60 | "The value of PAIR is :XBTEUR, which is not of type SIMPLE-STRING.")) 61 | ;; Test correct handling of COUNT keyword parameter to query params. 62 | (testing "when no COUNT is passed, it is absent from the query params" 63 | (let ((headers (with-output-to-string (*standard-output*) 64 | (cl-kraken:depth "xbteur" :verbose t)))) 65 | (ok (string= headers "Depth?pair=xbteur " :start1 65 :end1 83)))) 66 | (testing "when no COUNT is passed Kraken returns by default 100 asks and bids" 67 | (let ((pair (filter (cl-kraken:depth "xbteur") "result" "XXBTZEUR"))) 68 | (ok (= (length (filter pair "asks")) 100)) 69 | (ok (= (length (filter pair "bids")) 100)))) 70 | (testing "when passed a valid COUNT, count is present in the query params" 71 | (let* ((count (+ 1 (random 9))) 72 | (headers (with-output-to-string (*standard-output*) 73 | (cl-kraken:depth "xbteur" :count count :verbose t))) 74 | (expected (concatenate 'string "Depth?count=" (princ-to-string count) 75 | "&pair=xbteur "))) 76 | (ok (string= headers expected :start1 65 :end1 91)))) 77 | (testing "when passed a valid COUNT, evaluates to that number of asks/bids" 78 | (let* ((count (+ 1 (random 9))) 79 | (response (cl-kraken:depth "xbteur" :count count)) 80 | (pair (filter response "result" "XXBTZEUR")) 81 | (asks (filter pair "asks")) 82 | (bids (filter pair "bids"))) 83 | (ok (= (length asks) count)) 84 | (ok (= (length bids) count)))) 85 | ;; Test invalid COUNT values. 86 | (testing "when passed a COUNT of 0, ignores the value and evaluates depth" 87 | (let* ((count 0) 88 | (response (cl-kraken:depth "xbteur" :count count)) 89 | (error! (filter response "error")) 90 | (pair (filter response "result" "XXBTZEUR")) 91 | (asks (filter pair "asks")) 92 | (bids (filter pair "bids"))) 93 | (ok (null error!)) 94 | (ok (> (length asks) count)) 95 | (ok (> (length bids) count)))) 96 | (testing "when passed a COUNT of -1, ignores the value and evaluates depth" 97 | (let* ((count -1) 98 | (response (cl-kraken:depth "xbteur" :count count)) 99 | (error! (filter response "error")) 100 | (pair (filter response "result" "XXBTZEUR")) 101 | (asks (filter pair "asks")) 102 | (bids (filter pair "bids"))) 103 | (ok (null error!)) 104 | (ok (> (length asks) count)) 105 | (ok (> (length bids) count)))) 106 | (testing "when passed a string COUNT, a type error is signaled" 107 | (ok (signals (cl-kraken:depth "xbteur" :count "1") 'type-error) 108 | "The value of COUNT is \"1\", which is not of type INTEGER.")) 109 | (testing "when passed a symbol COUNT, a type error is signaled" 110 | (ok (signals (cl-kraken:depth "xbteur" :count 'a) 'type-error) 111 | "The value of COUNT is 'a, which is not of type INTEGER.")) 112 | (testing "when passed a keyword COUNT, a type error is signaled" 113 | (ok (signals (cl-kraken:depth "xbteur" :count :1) 'type-error) 114 | "The value of COUNT is :|1|, which is not of type INTEGER.")) 115 | ;; Test RAW parameter. 116 | (testing "when passed RAW T, evaluates to the raw response string" 117 | (let* ((response (cl-kraken:depth "xbtusd" :count 1 :raw t))) 118 | (ok (stringp response)) 119 | (ok (string= response "{\"error\":[],\"result\":{\"XXBTZUSD\":{\"asks\":[" 120 | :start1 0 :end1 42)))) 121 | (testing "when passed RAW NIL, evaluates as if no RAW argument was passed" 122 | (let* ((response (cl-kraken:depth "xbteur" :count 1 :raw nil)) 123 | (error! (filter response "error")) 124 | (result (filter response "result"))) 125 | (ok (consp response)) 126 | (ok (= (length response) 3)) 127 | (ok (eq (first response) :OBJ)) 128 | (ok (equal (second response) '("error"))) 129 | (ok (null error!)) 130 | (ok (consp result)))) 131 | ;; Test invalid RAW values. 132 | (testing "when passed a string RAW, a type error is signaled" 133 | (ok (signals (cl-kraken:depth "xbteur" :raw "1") 'type-error) 134 | "The value of RAW is \"1\", which is not of type (MEMBER T NIL).")) 135 | (testing "when passed a symbol RAW, a type error is signaled" 136 | (ok (signals (cl-kraken:depth "xbteur" :raw 'a) 'type-error) 137 | "The value of RAW is 'a, which is not of type (MEMBER T NIL).")) 138 | (testing "when passed a keyword RAW, a type error is signaled" 139 | (ok (signals (cl-kraken:depth "xbteur" :raw :1) 'type-error) 140 | "The value of RAW is :|1|, which is not of type (MEMBER T NIL)."))) 141 | -------------------------------------------------------------------------------- /tests/ohlc.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/tests/ohlc.lisp 2 | 3 | (defpackage #:cl-kraken/tests/ohlc 4 | (:use #:cl #:cl-kraken #:rove) 5 | (:import-from #:local-time 6 | #:now 7 | #:timestamp-to-unix) 8 | (:import-from #:jsown 9 | #:filter) 10 | (:import-from #:parse-float 11 | #:parse-float)) 12 | (in-package #:cl-kraken/tests/ohlc) 13 | 14 | (deftest ohlc 15 | (let* ((unix-now (timestamp-to-unix (now))) 16 | (since (princ-to-string unix-now))) 17 | (testing "when passed \"xBteuR\", evaluates to XBTEUR OHLC data" 18 | (let* ((response (cl-kraken:ohlc "XBTeUr" :since unix-now)) 19 | (error! (filter response "error")) 20 | (result (filter response "result")) 21 | (pair (filter result "XXBTZEUR")) 22 | (ohlc (car pair)) 23 | (time (first ohlc)) 24 | (open (second ohlc)) 25 | (high (third ohlc)) 26 | (low (fourth ohlc)) 27 | (close (fifth ohlc)) 28 | (vwap (sixth ohlc)) 29 | (volume (seventh ohlc)) 30 | (count (eighth ohlc)) 31 | (last (filter result "last")) 32 | (high-price (parse-float high)) 33 | (low-price (parse-float low))) 34 | (ok (consp response)) 35 | (ok (= (length response) 3)) 36 | (ok (null error!)) 37 | (ok (consp result)) 38 | (ok (= (length result) 3)) 39 | (ok (consp pair)) 40 | (ok (= (length pair) 1)) 41 | (ok (consp ohlc)) 42 | (ok (= (length ohlc) 8)) 43 | (ok (integerp time)) 44 | (ok (simple-string-p open)) 45 | (ok (simple-string-p high)) 46 | (ok (simple-string-p low)) 47 | (ok (simple-string-p close)) 48 | (ok (simple-string-p vwap)) 49 | (ok (simple-string-p volume)) 50 | (ok (integerp count)) 51 | (ok (integerp last)) 52 | (ok (floatp high-price)) 53 | (ok (floatp low-price)) 54 | (ok (>= high-price low-price)))) 55 | ;; Test correct handling of keyword parameters to query params. 56 | (testing "when passed no INTERVAL or SINCE, queries default interval of 1" 57 | (let ((headers (with-output-to-string (*standard-output*) 58 | (cl-kraken:ohlc "xbteur" :verbose t))) 59 | (expected "OHLC?pair=xbteur&interval=1 ")) 60 | (ok (string= headers expected :start1 65 :end1 93)))) 61 | (testing "when passed a valid INTERVAL, queries specified interval" 62 | (let ((headers (with-output-to-string (*standard-output*) 63 | (cl-kraken:ohlc "xbteur" :interval 21600 :verbose t))) 64 | (expected "OHLC?pair=xbteur&interval=21600 ")) 65 | (ok (string= headers expected :start1 65 :end1 97)))) 66 | (testing "when passed a valid SINCE, queries since + default interval of 1" 67 | (let ((headers (with-output-to-string (*standard-output*) 68 | (cl-kraken:ohlc "xbteur" :since unix-now :verbose t))) 69 | (expected (concatenate 'string "OHLC?since=" since 70 | "&pair=xbteur&interval=1 "))) 71 | (ok (string= headers expected :start1 65 :end1 110)))) 72 | (testing "when passed a valid SINCE+INTERVAL, queries both specified values" 73 | (let ((headers (with-output-to-string (*standard-output*) 74 | (cl-kraken:ohlc "xbteur" :since unix-now :interval 21600 75 | :verbose t))) 76 | (expected (concatenate 'string "OHLC?since=" since 77 | "&pair=xbteur&interval=21600 "))) 78 | (ok (string= headers expected :start1 65 :end1 114))))) 79 | ;; Test invalid PAIR values. 80 | (testing "when passed a multiple PAIR, evaluates to unknown asset pair error" 81 | (ok (equal (cl-kraken:ohlc "xbteur,xbtusd") 82 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 83 | (testing "when passed an invalid PAIR, evaluates to unknown asset pair error" 84 | (ok (equal (cl-kraken:ohlc "abc") 85 | '(:OBJ ("error" "EQuery:Unknown asset pair"))))) 86 | (testing "when passed an empty PAIR, evaluates to invalid arguments error" 87 | (ok (equal (cl-kraken:ohlc "") 88 | '(:OBJ ("error" "EGeneral:Invalid arguments"))))) 89 | (testing "when passed a symbol PAIR, a type error is signaled" 90 | (ok (signals (cl-kraken:ohlc 'xbteur) 'type-error) 91 | "The value of PAIR is XBTEUR, which is not of type SIMPLE-STRING.")) 92 | (testing "when passed a keyword PAIR, a type error is signaled" 93 | (ok (signals (cl-kraken:ohlc :xbteur) 'type-error) 94 | "The value of PAIR is :XBTEUR, which is not of type SIMPLE-STRING.")) 95 | ;; Test invalid INTERVAL values. 96 | (testing "when passed an invalid INTERVAL, returns an invalid arguments error" 97 | (ok (equal (cl-kraken:ohlc "xbteur" :interval 0) 98 | '(:OBJ ("error" "EGeneral:Invalid arguments"))))) 99 | (testing "when passed a string INTERVAL, a type error is signaled" 100 | (ok (signals (cl-kraken:ohlc "xbteur" :interval "1") 'type-error) 101 | "The value of INTERVAL is \"1\", which is not of type INTEGER.")) 102 | (testing "when passed a symbol INTERVAL, a type error is signaled" 103 | (ok (signals (cl-kraken:ohlc "xbteur" :interval 'a) 'type-error) 104 | "The value of INTERVAL is 'a, which is not of type INTEGER.")) 105 | (testing "when passed a keyword INTERVAL, a type error is signaled" 106 | (ok (signals (cl-kraken:ohlc "xbteur" :interval :1) 'type-error) 107 | "The value of INTERVAL is :|1|, which is not of type INTEGER.")) 108 | ;; Test invalid SINCE values. 109 | (testing "when passed a string SINCE, a type error is signaled" 110 | (ok (signals (cl-kraken:ohlc "xbteur" :since "1") 'type-error) 111 | "The value of SINCE is \"1\", which is not of type (OR INTEGER NULL.")) 112 | (testing "when passed a symbol SINCE, a type error is signaled" 113 | (ok (signals (cl-kraken:ohlc "xbteur" :since 'a) 'type-error) 114 | "The value of SINCE is 'a, which is not of type (OR INTEGER NULL).")) 115 | (testing "when passed a keyword SINCE, a type error is signaled" 116 | (ok (signals (cl-kraken:ohlc "xbteur" :since :1) 'type-error) 117 | "The value of SINCE is :|1|, which is not of type (OR INTEGER NULL).")) 118 | ;; Test RAW parameter. 119 | (testing "when passed RAW T, evaluates to the raw response string" 120 | (let ((response (cl-kraken:ohlc "xbtusd" 121 | :since (timestamp-to-unix (now)) :raw t)) 122 | (expected "{\"error\":[],\"result\":{\"XXBTZUSD\":[[")) 123 | (ok (stringp response)) 124 | (ok (string= response expected :start1 0 :end1 35)))) 125 | (testing "when passed RAW NIL, evaluates as if no RAW argument was passed" 126 | (let* ((response (cl-kraken:ohlc "xbteur" 127 | :since (timestamp-to-unix (now)) :raw nil)) 128 | (error! (filter response "error")) 129 | (result (filter response "result"))) 130 | (ok (consp response)) 131 | (ok (= (length response) 3)) 132 | (ok (eq (first response) :OBJ)) 133 | (ok (equal (second response) '("error"))) 134 | (ok (null error!)) 135 | (ok (consp result)))) 136 | ;; Test invalid RAW values. 137 | (testing "when passed a string RAW, a type error is signaled" 138 | (ok (signals (cl-kraken:ohlc "xbteur" :raw "1") 'type-error) 139 | "The value of RAW is \"1\", which is not of type (MEMBER T NIL).")) 140 | (testing "when passed a symbol RAW, a type error is signaled" 141 | (ok (signals (cl-kraken:ohlc "xbteur" :raw 'a) 'type-error) 142 | "The value of RAW is 'a, which is not of type (MEMBER T NIL).")) 143 | (testing "when passed a keyword RAW, a type error is signaled" 144 | (ok (signals (cl-kraken:ohlc "xbteur" :raw :1) 'type-error) 145 | "The value of RAW is :|1|, which is not of type (MEMBER T NIL)."))) 146 | -------------------------------------------------------------------------------- /src/main.lisp: -------------------------------------------------------------------------------- 1 | ;;;; cl-kraken/src/main.lisp 2 | 3 | (in-package #:cl-user) 4 | (defpackage #:cl-kraken 5 | (:documentation 6 | "CL-Kraken is Common Lisp client for the Kraken cryptocurrency exchange. 7 | Copyright (c) 2019-2020 Jon Atack . See LICENSE for details. 8 | The Kraken API is documented here: https://www.kraken.com/help/api.") 9 | (:nicknames #:cl-kraken/src/main) 10 | (:use #:cl) 11 | (:shadow #:dexador) 12 | (:shadowing-import-from #:cl-kraken/src/http #:request) 13 | (:export 14 | ;; Public API 15 | #:asset-pairs 16 | #:assets 17 | #:depth 18 | #:ohlc 19 | #:server-time 20 | #:spread 21 | #:ticker 22 | #:trades 23 | ;; Private API 24 | #:balance 25 | #:trade-balance 26 | #:trade-volume)) 27 | (in-package #:cl-kraken) 28 | 29 | (eval-when (:compile-toplevel :load-toplevel :execute) 30 | (declaim (optimize (speed 2) (safety 3) (debug 3)))) 31 | 32 | ;;; API 33 | ;;; 34 | ;;; All API calls accept the following optional boolean keyword parameters: 35 | ;;; 36 | ;;; - RAW (T or default NIL) to return the JSON response as a raw string 37 | ;;; instead of parsed to a list data structure. 38 | ;;; 39 | ;;; - VERBOSE (T or default NIL) to output the HTTP request headers for 40 | ;;; verifying and debugging. 41 | ;;; 42 | ;;; Kraken Public API 43 | ;;; 44 | (defun asset-pairs (&key pair raw verbose) 45 | "Get tradeable asset pairs. 46 | URL: https://api.kraken.com/0/public/AssetPairs 47 | Input: 48 | `pair' = optional, comma-delimited, case-insensitive asset pair string; 49 | if not provided, defaults to all pairs. 50 | Kraken returns a hash with keys `error' and `result'. 51 | `result' is a hash of pair keys, each with a values hash containing: 52 | `altname' = alternate pair name 53 | `aclass_base' = asset class of base component 54 | `base' = asset id of base component 55 | `aclass_quote' = asset class of quote component 56 | `quote' = asset id of quote component 57 | `lot' = volume lot size 58 | `pair_decimals' = scaling decimal places for pair 59 | `lot_decimals' = scaling decimal places for volume 60 | `lot_multiplier' = multiply lot volume by this to get currency volume 61 | `leverage_buy' = array of leverage amounts available when buying 62 | `leverage_sell' = array of leverage amounts available when selling 63 | `fees' = fee schedule array in [volume, percent fee] tuples 64 | `fees_maker' = maker fee schedule array in [volume, percent fee] 65 | tuples (if on maker/taker) 66 | `fee_volume_currency' = volume discount currency 67 | `margin_call' = margin call level 68 | `margin_stop' = stop-out/liquidation margin level 69 | If an asset pair is on a maker/taker fee schedule, the taker side is given in 70 | `fees' and maker side in `fees_maker'. 71 | For asset pairs not on maker/taker, the rates will only be given in `fees'." 72 | (declare (type boolean raw verbose)) 73 | (check-type pair (or string null)) 74 | (request "AssetPairs" :params (when (stringp pair) `(("pair" . ,pair))) 75 | :raw raw :verbose verbose)) 76 | 77 | (defun assets (&key asset raw verbose) 78 | "Get asset info. 79 | URL: https://api.kraken.com/0/public/Assets 80 | Input: 81 | `asset' = optional, comma-delimited, case-insensitive asset list string; 82 | if not provided, defaults to all assets. 83 | Kraken returns a hash with keys `error' and `result'. 84 | `result' is a hash of asset name keys, each with a values hash containing: 85 | `altname' = alternate name, like EUR, USD, XBT 86 | `aclass' = asset class, currently always set to 'currency' 87 | `decimals' = decimal places for record keeping 88 | `display_decimals' = decimal places for display (usually fewer)" 89 | (declare (type boolean raw verbose)) 90 | (check-type asset (or string null)) 91 | (request "Assets" :params (when (stringp asset) `(("asset" . ,asset))) 92 | :raw raw :verbose verbose)) 93 | 94 | (defun depth (pair &key count raw verbose) 95 | "Get order book public price data for an asset pair. 96 | URL: https://api.kraken.com/0/public/Depth 97 | Input: 98 | PAIR = required single asset pair for which to query order book 99 | COUNT = optional integer of maximum asks and bids to receive 100 | Kraken returns a hash with keys `error' and `result'. 101 | `result' is an array containing a pair name and the keys `asks' and `bids' 102 | each followed by an array of `price', `volume', and `timestamp>'." 103 | (declare (type boolean raw verbose)) 104 | #+(or sbcl ccl ecl abcl) (declare (type simple-string pair)) 105 | #+(or sbcl ccl) (declare (type (or integer null) count)) 106 | #+clisp (check-type pair simple-string) 107 | #+(or abcl clisp ecl) (check-type count (or integer null)) 108 | (let ((params `(("pair" . ,pair)))) 109 | (when (integerp count) (push (cons "count" count) params)) 110 | (request "Depth" :params params :raw raw :verbose verbose))) 111 | 112 | (defun ohlc (pair &key since (interval 1) raw verbose) 113 | "Get OHLC (Open, High, Low, Close) public price data for an asset pair. 114 | URL: https://api.kraken.com/0/public/OHLC 115 | Input: 116 | PAIR = required single asset pair for which to query OHLC data 117 | INTERVAL = optional integer time interval in minutes defaulting to 1, 118 | permitted values are 1, 5, 15, 30, 60, 240, 1440, 10080, 21600; 119 | Kraken returns an Invalid Arguments error for other values 120 | SINCE = optional integer Unix Time id from when to return new committed 121 | OHLC data, corresponding to previous OHLC `last' values. 122 | Kraken returns a hash with keys `error' and `result'. 123 | `result' is an array containing a pair name and a `last' Unix Time id. 124 | - The pair name contains an array of arrays, each containing values for 125 | `time', `open', `high', `low', `close', `VWAP', `volume' and `count'. 126 | - `last' is a Unix Time id for the current not-yet-committed frame. 127 | Useful as value for SINCE when querying for new committed OHLC data." 128 | (declare (type boolean raw verbose)) 129 | #+(or sbcl ccl ecl abcl) (declare (type simple-string pair)) 130 | #+(or sbcl ccl) (declare (type (or integer null) since) 131 | (type integer interval)) 132 | #+clisp (check-type pair simple-string) 133 | #+(or abcl clisp ecl) (check-type since (or integer null)) 134 | #+(or abcl clisp ecl) (check-type interval integer) 135 | (let ((params `(("pair" . ,pair) ("interval" . ,interval)))) 136 | (when (integerp since) (push (cons "since" since) params)) 137 | (request "OHLC" :params params :raw raw :verbose verbose))) 138 | 139 | (defun server-time (&key raw verbose) 140 | "Get server time. Useful to approximate skew time between server and client. 141 | URL: https://api.kraken.com/0/public/Time 142 | Kraken returns a hash with keys `error' and `result'. 143 | `result' is an array of hashes with keys: 144 | `unixtime' = unix timestamp 145 | `rfc1123' = RFC 1123 time format" 146 | (declare (type boolean raw verbose)) 147 | (request "Time" :raw raw :verbose verbose)) 148 | 149 | (defun spread (pair &key since raw verbose) 150 | "Get recent spread data for an asset pair. 151 | URL: https://api.kraken.com/0/public/Spread 152 | Input: 153 | PAIR = required single asset pair for which to query spread data 154 | SINCE = optional integer Unix Time id from when to return spread data, 155 | corresponding to previous spread `last' values. 156 | Note: SINCE is inclusive, so any returned data with the same time as 157 | a previous set should overwrite all of the previous set's entries. 158 | Kraken returns a hash with keys `error' and `result'. 159 | `result' is an array containing a pair name and a `last' Unix Time id. 160 | - The pair name contains an array of arrays for `time', `bid', and `ask'. 161 | - `last' is a Unix Time id to use for SINCE when querying new spread." 162 | (declare (type boolean raw verbose)) 163 | #+(or sbcl ccl ecl abcl) (declare (type simple-string pair)) 164 | #+(or sbcl ccl) (declare (type (or integer null) since)) 165 | #+clisp (check-type pair simple-string) 166 | #+(or abcl clisp ecl) (check-type since (or integer null)) 167 | (let ((params `(("pair" . ,pair)))) 168 | (when (integerp since) (push (cons "since" since) params)) 169 | (request "Spread" :params params :raw raw :verbose verbose))) 170 | 171 | (defun ticker (pair &key raw verbose) 172 | "Get ticker data for asset pairs. 173 | URL: https://api.kraken.com/0/public/Ticker 174 | Input: 175 | `pair' = required comma-delimited list of one or more asset pairs. 176 | Kraken returns a hash with keys `error' and `result'. 177 | `result' is a hash of pair keys, each with a values hash of ticker data: 178 | a = ask array (price, whole lot volume, lot volume) 179 | b = bid array (price, whole lot volume, lot volume) 180 | c = last trade closed array (price, lot volume) 181 | v = volume array (today, last 24 hours) 182 | p = volume weighted avg price array (today, last 24 hours) 183 | t = number of trades array (today, last 24 hours) 184 | l = low array (today, last 24 hours) 185 | h = high array (today, last 24 hours) 186 | o = opening price (today, at 00:00:00 UTC)" 187 | (declare (type boolean raw verbose)) 188 | (check-type pair (and string (not null))) 189 | (request "Ticker" :params `(("pair" . ,pair)) :raw raw :verbose verbose)) 190 | 191 | (defun trades (pair &key since raw verbose) 192 | "Get recent trades public price data for an asset pair. 193 | URL: https://api.kraken.com/0/public/Trades 194 | Input: 195 | PAIR = required single asset pair for which to query trades data 196 | SINCE = optional integer timestamp id from when to return new trades data, 197 | corresponding to previous trades `last' values. 198 | Kraken returns a hash with keys `error' and `result'. 199 | `result' is an array containing a pair name and a `last' Unix Time id. 200 | - The pair name is followed by an array of maximum 1000 trades containing 201 | `price', `volume', `time', `buy/sell', `market/limit', `miscellaneous'. 202 | - `last' is a timestamp id to use for SINCE when querying new trades." 203 | (declare (type boolean raw verbose)) 204 | #+(or sbcl ccl ecl abcl) (declare (type simple-string pair)) 205 | #+(or sbcl ccl) (declare (type (or integer null) since)) 206 | #+clisp (check-type pair simple-string) 207 | #+(or abcl clisp ecl) (check-type since (or integer null)) 208 | (let ((params `(("pair" . ,pair)))) 209 | (when (integerp since) (push (cons "since" since) params)) 210 | (request "Trades" :params params :raw raw :verbose verbose))) 211 | 212 | ;;; 213 | ;;; Kraken Private API requiring authentication 214 | ;;; 215 | (defun balance (&key raw verbose) 216 | (declare (type boolean raw verbose)) 217 | (request "Balance" :post t :raw raw :verbose verbose)) 218 | 219 | (defun trade-balance (&key raw verbose) 220 | (declare (type boolean verbose)) 221 | (request "TradeBalance" :post t :raw raw :verbose verbose)) 222 | 223 | (defun trade-volume (&key pair fee-info raw verbose) 224 | "Get trade volume. 225 | URL: https://api.kraken.com/0/private/TradeVolume 226 | Input: 227 | PAIR = optional, comma-delimited, case-insensitive asset pair string; 228 | if not provided, defaults to all pairs. 229 | FEE-INFO = optional boolean whether or not to include fee info in results. 230 | Note: Currently appears to be not implemented by Kraken, since 231 | the fee info is always returned. 232 | Kraken returns a hash with keys `error' and `result'. 233 | `result' is a hash of pair keys, each with a values hash containing: 234 | `currency' = volume currency 235 | `volume' = current discount volume 236 | `fees' = array of asset pairs with *taker* fee tier data 237 | `fee' = current fee in percent 238 | `minfee' = minimum fee for pair (if not fixed fee) 239 | `maxfee' = maximum fee for pair (if not fixed fee) 240 | `nextfee' = next tier's fee for pair 241 | (if not fixed fee, nil if at lowest fee tier) 242 | `nextvolume' = volume level of next tier 243 | (if not fixed fee, nil if at lowest fee tier) 244 | `tiervolume' = volume level of current tier 245 | (if not fixed fee, nil if at lowest fee tier) 246 | `fees_maker' = array of asset pairs with *maker* fee tier data for any 247 | pairs on a maker/taker fee schedule 248 | `fee' = current fee in percent 249 | `minfee' = minimum fee for pair (if not fixed fee) 250 | `maxfee' = maximum fee for pair (if not fixed fee) 251 | `nextfee' = next tier's fee for pair 252 | (if not fixed fee, nil if at lowest fee tier) 253 | `nextvolume' = volume level of next tier 254 | (if not fixed fee, nil if at lowest fee tier) 255 | `tiervolume' = volume level of current tier 256 | (if not fixed fee, nil if at lowest fee tier) 257 | If an asset pair is on a maker/taker fee schedule, the taker side is given in 258 | `fees' and maker side in `fees_maker'. 259 | For asset pairs not on maker/taker, the rates will only be given in `fees'." 260 | (declare (type boolean fee-info raw verbose)) 261 | #+(or sbcl ccl) (declare (type (or simple-string null) pair)) 262 | #+(or abcl clisp ecl) (check-type pair (or simple-string null)) 263 | (let ((params)) 264 | (when (stringp pair) (push (cons "pair" pair) params)) 265 | (when fee-info (push (list "fee-info") params)) 266 | (request "TradeVolume" :params params :post t :raw raw :verbose verbose))) 267 | --------------------------------------------------------------------------------