Initial checkin
This commit is contained in:
commit
ec1ddf50b4
|
@ -0,0 +1,10 @@
|
|||
.DS_Store
|
||||
.idea
|
||||
*.log
|
||||
tmp/
|
||||
|
||||
.cpcache/
|
||||
.nrepl-port
|
||||
target/
|
||||
result
|
||||
.clj-kondo
|
File diff suppressed because it is too large
Load Diff
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 = { };
|
||||
};
|
||||
}
|
|
@ -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 }))
|
Loading…
Reference in New Issue