Initial checkin

This commit is contained in:
niten 2022-07-07 16:04:23 -07:00
commit ec1ddf50b4
6 changed files with 1544 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.DS_Store
.idea
*.log
tmp/
.cpcache/
.nrepl-port
target/
result
.clj-kondo

1088
deps-lock.json Normal file

File diff suppressed because it is too large Load Diff

43
deps.edn Normal file
View File

@ -0,0 +1,43 @@
{
:paths ["src"]
:deps {
org.clojure/clojure { :mvn/version "1.11.1" }
org.fudo/bebot {
:git/url "https://git.fudo.org/fudo-public/bebot.git"
:sha "6cda6ea9d4f2e0b751e88072134264fb888c8114"
}
org.fudo/fudo-clojure {
:git/url "https://git.fudo.org/fudo-public/fudo-clojure.git"
:sha "c6a1ebef2e5b64d432a46ac48639c674e62b7cee"
}
org.fudo/coinbase-pro-client {
:git/url "https://git.fudo.org/fudo-public/coinbase-pro-client.git"
:sha "968c4aeeebe42a574a622b955d0c981ea6b5dc36"
}
org.clojure/math.numeric-tower { :mvn/version "0.0.5" }
org.clojure/tools.cli { :mvn/version "1.0.206" }
camel-snake-kebab/camel-snake-kebab { :mvn/version "0.4.2" }
}
:aliases {
:run {
:main-opts ["-m" "chute.core"]
}
:lint {
:replace-deps { clj-kondo/clj-kondo {:mvn/version "2022.04.25" } }
:main-opts ["-m" "clj-kondo.main" "--lint" "src"]
}
:cache-lint {
:replace-deps { clj-kondo/clj-kondo { :mvn/version "2022.04.25" } }
:main-opts ["-m" "clj-kondo.main"
"--lint" "src"
"--dependencies"
"--parallel"
"--copy-configs"]
}
}
}

117
flake.lock Normal file
View File

@ -0,0 +1,117 @@
{
"nodes": {
"clj-nix": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1655801580,
"narHash": "sha256-4XUFDP1ES1KNWwDukQEixCe4uV7Z951kgaVAFhXI2ew=",
"owner": "jlesquembre",
"repo": "clj-nix",
"rev": "579141e009200fcd28d251731e9ac5ba46a1ec2a",
"type": "github"
},
"original": {
"owner": "jlesquembre",
"repo": "clj-nix",
"type": "github"
}
},
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"clj-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1644227066,
"narHash": "sha256-FHcFZtpZEWnUh62xlyY3jfXAXHzJNEDLDzLsJxn+ve0=",
"owner": "numtide",
"repo": "devshell",
"rev": "7033f64dd9ef8d9d8644c5030c73913351d2b660",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1642700792,
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1644229661,
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1655643053,
"narHash": "sha256-8PMDEr44DwH45PbBijtQcAPyC4Ap+sOO5wK0y5lFvyY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5afb1b7dcf46c4ded5719525a42879b35363862c",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-22.05",
"type": "indirect"
}
},
"root": {
"inputs": {
"clj-nix": "clj-nix",
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"utils": {
"locked": {
"lastModified": 1653893745,
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

35
flake.nix Normal file
View File

@ -0,0 +1,35 @@
{
description = "Chute - Cryptocurrency Parachute.";
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 = import nixpkgs { inherit system; };
mkCljBin = clj-nix.packages."${system}".mkCljBin;
update-deps = pkgs.writeShellScriptBin "update-deps.sh" ''
${clj-nix.packages."${system}".deps-lock}/bin/deps-lock
'';
in {
packages.chute = mkCljBin {
projectSrc = ./.;
name = "org.fudo/chute";
main-ns = "chute.core";
jdkRunner = pkgs.jdk17_headless;
version = "0.1";
};
devShell =
pkgs.mkShell { buildInputs = with pkgs; [ clojure update-deps ]; };
})) // {
nixosModule = { };
};
}

251
src/chute/harness.clj Normal file
View File

@ -0,0 +1,251 @@
(ns chute.harness
(:require [fudo-clojure.logging :as log]
[fudo-clojure.result :refer [dispatch-result let-result success map-success failure]]
[exchange.client :as client]
[exchange.order :as order]
[exchange.account :as acct]
[clojure.spec.alpha :as s])
(:import java.time.Duration))
(s/def ::stage
#{::initialize
::get-historical-sticky-price
::create-order
::create-buy-order
::create-sell-order
::create-stop-loss
::create-stop-gain
::alert-market-price-too-high
::alert-market-price-too-low
::reconsider-stop-loss
::cancel-order
::ensure-order-cancelled
::handle-error})
(s/def ::prev-stage ::stage)
(s/def ::stage-args
(s/keys :opt [::order-id ::order ::market-price ::currency-balance ::base-balance]))
(s/def ::order-id uuid?)
(s/def ::order order/order?)
(s/def ::market-price decimal?)
(s/def ::currency-balance decimal?)
(s/def ::base-balance decimal?)
(s/def ::sticky-price (s/nilable decimal?))
(s/def ::state
(s/keys :req [::stage
::prev-stage
::stage-args
::client
::sticky-price
::currency
::base-currency
::logger
::stop-loss-percentile
::format-$
::delay]))
(s/def ::delay (partial instance? Duration))
(defn- next-stage [state stage & remaining-args]
(let [args (apply hash-map remaining-args)
stage-delay (get args ::delay (Duration/ofSeconds 30))]
(-> state
(assoc ::prev-stage (::stage state)
::delay stage-delay
::stage stage
::stage-args (dissoc args ::delay))
(success))))
(defn- handle-error [state e]
(next-stage state ::handle-error :error e))
(defn- with-sticky-price [state price]
(assoc state ::sticky-price price))
(defn- get-state [state k]
(if-let [val (get state k)]
val
(throw (ex-info (str "missing state key: " k)
{::state state
::key k}))))
(defn- get-arg [state arg]
(if-let [val (get-in state [::stage-args arg])]
val
(throw (ex-info (str "missing argument: " arg)
{::args (::stage-args state)
::key arg}))))
;; TODO: add explicit namespace to get-state
(defmacro with-state [state bindings & body]
(let [namespace-key (fn [sym] (keyword (str *ns*) (name sym)))
fetch-key (fn [k] `(get-state ~state ~(namespace-key k)))
bind-clause (vec (mapcat (juxt identity fetch-key) bindings))]
`(let ~bind-clause
~@body)))
;; TODO: add explicit namespace to get-arg
(defmacro with-args [state bindings & body]
(let [fetch-key (fn [k] `(get-arg ~state ~k))
bind-clause (vec (mapcat (fn [[k v]] [k (fetch-key v)]) bindings))]
`(let ~bind-clause
~@body)))
(defmulti stage:execute ::stage)
(defn execute! [init-state]
(loop [state init-state]
(let [delay (::delay state)]
(Thread/sleep (.getSeconds delay))
(dispatch-result (stage:execute (dissoc state ::delay))
([next-state] (recur next-state))
([err] (recur (handle-error state err)))))))
(defmethod stage:execute ::initialize [state]
(with-state state [sticky-price]
(if (nil? sticky-price)
(next-stage state ::get-historical-sticky-price)
(next-stage state ::create-order))))
(defmethod stage:execute ::get-historical-sticky-price [state]
(with-state state [client currency]
(let-result [orders (client/get-completed-limit-orders! client currency)]
(let [price (-> orders first order/price)]
(next-stage (with-sticky-price state price)
::create-order)))))
(defmethod stage:execute ::create-order [state]
(with-state state [client currency base-currency]
(let-result [accts (client/get-accounts! client)
market-price (client/get-market-price! client currency)]
(let [currency-balance (acct/account-balance accts currency)
base-value (acct/account-balance accts base-currency)
currency-value (* market-price currency-balance)]
(if (> currency-value base-value)
(next-stage state ::create-sell-order
::market-price market-price
::currency-balance currency-balance)
(next-stage state ::create-buy-order
::market-price market-price
::base-balance base-value))))))
(defmethod stage:execute ::create-sell-order [state]
(with-state state [client sticky-price]
(with-args state {market-price ::market-price
currency-balance ::currency-balance}
(if (> market-price sticky-price)
(next-stage state ::create-stop-loss
::currency-balance currency-balance)
(next-stage state ::alert-market-price-too-low
::currency-balance currency-balance
::market-price market-price)))))
(defmethod stage:execute ::create-buy-order [state]
(with-state state [client sticky-price]
(with-args state {market-price ::market-price
base-balance ::base-balance}
(if (> market-price sticky-price)
(next-stage state ::create-immediate-buy
::base-balance base-balance
::market-price market-price)
(next-stage state ::alert-market-price-too-high
::base-balance base-balance
::market-price market-price)))))
(defmethod stage:execute ::create-stop-loss [state]
(with-state state [client currency sticky-price]
(with-args state {currency-balance ::currency-balance}
(let-result [order-id (client/create-stop-loss-order! client currency stop-price sticky-price currency-balance)]
(next-stage state ::watch-stop-loss ::order-id order-id)))))
(defmethod stage:execute ::alert-market-price-too-low [state]
(with-state state [logger sticky-price format-$]
(with-args state {market-price ::market-price}
(log/alert! logger (str "FAILED TO CREATE STOP LOSS!"
\newline
" Market price too low!"
\newline
" Current market price: " (format-$ market-price)
\newline
" Sticky price: " (format-$ sticky-price)))
(next-stage state ::create-order))))
(defmethod stage:execute ::alert-market-price-too-high [state]
(with-state state [logger sticky-price format-$]
(with-args state {market-price ::market-price}
(log/alert! logger (str "FAILED TO CREATE STOP GAIN!"
\newline
" Market price too high!"
\newline
" Current market price: " (format-$ market-price)
\newline
" Sticky price: " (format-$ sticky-price)))
(next-stage state ::create-order))))
(defmethod stage:execute ::create-stop-gain [state]
(with-state state [client currency sticky-price]
(with-args state {base-balance ::base-balance}
(let-result [order-id (client/create-stop-gain-order! client currency stop-price sticky-price base-balance)]))))
(defmethod stage:execute ::watch-stop-loss [state]
(with-state state [client]
(with-args state {order-id ::order-id}
(let-result [order (client/get-order! client order-id)]
(if (order/filled? order)
(next-stage state ::create-order)
(next-stage state ::reconsider-stop-loss
::order order))))))
(defmethod stage:execute ::calculate-sticky-price [state]
(with-args state {txn-price ::transaction-price
txn-quantity ::txn-quantity
fees ::fees}
(let [fee-share (/ fees txn-quantity)]
(next-stage (with-sticky-price state (+ txn-price fee-share))
::create-order))))
(defmethod stage:execute ::reconsider-stop-loss [state]
(with-state state [client currency stop-loss-percentile sticky-price]
(with-args state {order ::order}
(let-result [market-price (client/get-market-price! client currency)]
(let [target-price (* market-price stop-loss-percentile)
order-id (order/id order)]
(if (> target-price sticky-price)
(next-stage (with-sticky-price state target-price) ::cancel-order
::order-id order-id)
(next-stage state ::watch-stop-loss
::order-id order-id)))))))
(defmethod stage:execute ::cancel-order [state]
(with-state state [client]
(with-args state {order-id ::order-id}
(dispatch-result (client/cancel-order! client order-id)
([order-id] (next-stage state ::ensure-order-cancelled
::order-id order-id))
([error] (if (http/not-found-error? error)
(next-stage state ::create-order)
error))))))
(defmethod stage:execute ::ensure-order-cancelled [state]
(with-state state [client]
(with-args state {order-id ::order-id}
(dispatch-result (client/get-order! client order-id)
([order] (if (order/cancelled? order)
(next-stage state ::create-order)
(next-stage state ::cancel-order
::order-id order-id)))
([error] (if (http/not-found-error? error)
(next-stage state ::create-order)
error))))))
(defmethod stage:execute :default [state]
(failure (str "stage:execute reached unexpected state: " state)
{ :state state }))