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