Updates...

This commit is contained in:
niten 2024-03-23 11:23:14 -07:00
parent 9c6b0acaba
commit 11107baa6a
10 changed files with 778 additions and 273 deletions

View File

@ -1,6 +1,9 @@
{:paths ["src"] {:paths ["src"]
:deps :deps
{org.clojure/data.csv {:mvn/version "1.0.0"} {org.clojure/clojure {:mvn/version "1.11.0"}
org.clojure/data.csv {:mvn/version "1.0.1"}
org.clojure/core.async {:mvn/version "1.5.648"}
org.clojure/core.match {:mvn/version "1.0.0"} org.clojure/core.match {:mvn/version "1.0.0"}
org.clojure/tools.cli {:mvn/version "1.0.206"}
arachne-framework/valuehash {:git/url "https://git.fudo.org/fudo-public/valuehash.git" arachne-framework/valuehash {:git/url "https://git.fudo.org/fudo-public/valuehash.git"
:sha "9d2dbafdb5db886a57f44c5b7fe32c824713e6c7"}}} :sha "9d2dbafdb5db886a57f44c5b7fe32c824713e6c7"}}}

269
deps.nix
View File

@ -1,82 +1,54 @@
# generated by clj2nix-1.0.7 # generated by clj2nix-1.1.0-rc
{ pkgs ? import <nixpkgs> {} }: { fetchMavenArtifact, fetchgit, lib }:
let repos = [ let repos = [
"https://repo1.maven.org/maven2/" "https://repo1.maven.org/maven2/"
"https://repo.clojars.org/" ]; "https://repo.clojars.org/" ];
in rec { in rec {
fetchmaven = pkgs.callPackage (pkgs.fetchurl { makePaths = {extraClasspaths ? []}:
url = "https://raw.githubusercontent.com/NixOS/nixpkgs/ba5e2222458a52357a3ba5873d88779d5c223269/pkgs/build-support/fetchmavenartifact/default.nix"; if (builtins.typeOf extraClasspaths != "list")
sha512 = "05m7i8hbhyfz7p2f106mfbsasjf04svd9xkgc26pl3shljrk0dfacz39wiwzm6xqw7czgrsx745vciram7al621v7634nfdq3m1x88a"; then builtins.throw "extraClasspaths must be of type 'list'!"
}) {}; else (lib.concatMap (dep:
makePaths = {extraClasspaths ? null}: builtins.map (path:
(pkgs.lib.concatMap if builtins.isString path then
(dep: path
builtins.map else if builtins.hasAttr "jar" path then
(path: path.jar
if builtins.isString path then else if builtins.hasAttr "outPath" path then
path path.outPath
else if builtins.hasAttr "jar" path then else
path.jar path
else if builtins.hasAttr "outPath" path then )
path.outPath dep.paths)
else packages) ++ extraClasspaths;
path makeClasspaths = {extraClasspaths ? []}:
) if (builtins.typeOf extraClasspaths != "list")
dep.paths) then builtins.throw "extraClasspaths must be of type 'list'!"
packages) else builtins.concatStringsSep ":" (makePaths {inherit extraClasspaths;});
++ (if extraClasspaths != null then [ extraClasspaths ] else []);
makeClasspaths = {extraClasspaths ? null}: builtins.concatStringsSep ":" (makePaths {inherit extraClasspaths;});
packageSources = builtins.map (dep: dep.src) packages; packageSources = builtins.map (dep: dep.src) packages;
packages = [ packages = [
rec {
name = "data.csv/org.clojure";
src = fetchmaven {
inherit repos;
artifactId = "data.csv";
groupId = "org.clojure";
sha512 = "b039775a859ed27eca8f8ae74ccb6afde3ad1fe2b3cbe542240c324d60fe1237e495eb1300ee9eb4ff4ef59f01faf7aec6ef1dd6a025ee4fe556c1d91acfcf1b";
version = "1.0.0";
};
paths = [ src ];
}
rec {
name = "core.match/org.clojure";
src = fetchmaven {
inherit repos;
artifactId = "core.match";
groupId = "org.clojure";
sha512 = "52ada3bbe73ed1b429be811d3990df0cdb3e9d50f2a6c92b70d490a8ea922d4794da93c3b7487653f801954fc599704599b318b4d7926a9594583df37c55e926";
version = "1.0.0";
};
paths = [ src ];
}
(rec {
name = "arachne-framework/valuehash";
src = pkgs.fetchgit {
name = "valuehash";
url = "https://git.fudo.org/fudo-public/valuehash.git";
rev = "9d2dbafdb5db886a57f44c5b7fe32c824713e6c7";
sha256 = "1civ393c4yy9p2xbmrrvpbyqczx55k3fkvimkf850fl62ns5zl9r";
};
paths = map (path: src + path) [
"/src"
];
})
rec { rec {
name = "clojure/org.clojure"; name = "clojure/org.clojure";
src = fetchmaven { src = fetchMavenArtifact {
inherit repos; inherit repos;
artifactId = "clojure"; artifactId = "clojure";
groupId = "org.clojure"; groupId = "org.clojure";
sha512 = "d9e2c0676cdc349a3455d92b3ce3c3f01a2410de448c9416edfe72bc7eaf356cfadbb6d746740a821940c3b4cab100ca941e23bab482e98b404ed9ef79c562df"; sha512 = "efb87dfd347d2be6cb251d550e312d77797e35500b75ebe8e3fca824d16223803305ce89d4ae0349e5dff22a99c24b8719bf791f24685a12404bd56a44693010";
version = "1.10.0-alpha4"; version = "1.11.0";
};
paths = [ src ];
}
rec {
name = "tools.analyzer/org.clojure";
src = fetchMavenArtifact {
inherit repos;
artifactId = "tools.analyzer";
groupId = "org.clojure";
sha512 = "c51752a714848247b05c6f98b54276b4fe8fd44b3d970070b0f30cd755ac6656030fd8943a1ffd08279af8eeff160365be47791e48f05ac9cc2488b6e2dfe504";
version = "1.1.0";
}; };
paths = [ src ]; paths = [ src ];
@ -84,7 +56,7 @@ let repos = [
rec { rec {
name = "tools.logging/org.clojure"; name = "tools.logging/org.clojure";
src = fetchmaven { src = fetchMavenArtifact {
inherit repos; inherit repos;
artifactId = "tools.logging"; artifactId = "tools.logging";
groupId = "org.clojure"; groupId = "org.clojure";
@ -96,26 +68,169 @@ let repos = [
} }
rec { rec {
name = "spec.alpha/org.clojure"; name = "core.specs.alpha/org.clojure";
src = fetchmaven { src = fetchMavenArtifact {
inherit repos; inherit repos;
artifactId = "spec.alpha"; artifactId = "core.specs.alpha";
groupId = "org.clojure"; groupId = "org.clojure";
sha512 = "b8fc40ed9bc52b545e699ed188dd61bfd144ee67f0c70364b8f2715e9f1fea608d3721db7f618f6ef4bc3056e3c2984c626080486ca710f3595dda8ba23730ac"; sha512 = "f521f95b362a47bb35f7c85528c34537f905fb3dd24f2284201e445635a0df701b35d8419d53c6507cc78d3717c1f83cda35ea4c82abd8943cd2ab3de3fcad70";
version = "0.1.143"; version = "0.2.62";
}; };
paths = [ src ]; paths = [ src ];
} }
rec { rec {
name = "core.specs.alpha/org.clojure"; name = "spec.alpha/org.clojure";
src = fetchmaven { src = fetchMavenArtifact {
inherit repos; inherit repos;
artifactId = "core.specs.alpha"; artifactId = "spec.alpha";
groupId = "org.clojure"; groupId = "org.clojure";
sha512 = "b4f5eee01da39914e6024dd529d1f72952d5a9dae65e1e41bf386b1e86a004a0d197b5be95aa70e7e8d6438c92b7fa8fc0c5039f2013e97c0b91c22d86fb7968"; sha512 = "ddfe4fa84622abd8ac56e2aa565a56e6bdc0bf330f377ff3e269ddc241bb9dbcac332c13502dfd4c09c2c08fe24d8d2e8cf3d04a1bc819ca5657b4e41feaa7c2";
version = "0.1.24"; version = "0.3.218";
};
paths = [ src ];
}
rec {
name = "tools.cli/org.clojure";
src = fetchMavenArtifact {
inherit repos;
artifactId = "tools.cli";
groupId = "org.clojure";
sha512 = "1d88aa03eb6a664bf2c0ce22c45e7296d54d716e29b11904115be80ea1661623cf3e81fc222d164047058239010eb678af92ffedc7c3006475cceb59f3b21265";
version = "1.0.206";
};
paths = [ src ];
}
rec {
name = "tools.analyzer.jvm/org.clojure";
src = fetchMavenArtifact {
inherit repos;
artifactId = "tools.analyzer.jvm";
groupId = "org.clojure";
sha512 = "36ad50a7a79c47dea16032fc4b927bd7b56b8bedcbd20cc9c1b9c85edede3a455369b8806509b56a48457dcd32e1f708f74228bce2b4492bd6ff6fc4f1219d56";
version = "1.2.2";
};
paths = [ src ];
}
rec {
name = "asm/org.ow2.asm";
src = fetchMavenArtifact {
inherit repos;
artifactId = "asm";
groupId = "org.ow2.asm";
sha512 = "876eac7406e60ab8b9bd6cd3c221960eaa53febea176a88ae02f4fa92dbcfe80a3c764ba390d96b909c87269a30a69b1ee037a4c642c2f535df4ea2e0dd499f2";
version = "9.2";
};
paths = [ src ];
}
rec {
name = "data.csv/org.clojure";
src = fetchMavenArtifact {
inherit repos;
artifactId = "data.csv";
groupId = "org.clojure";
sha512 = "6b667a56cbb6632a90564f217f9c28a6670c13c729fb205ced091d9ee006382143dd6b615dd5a4f900660946199cac449fe9fabc90820bb34b92a9e6c8550473";
version = "1.0.1";
};
paths = [ src ];
}
rec {
name = "core.match/org.clojure";
src = fetchMavenArtifact {
inherit repos;
artifactId = "core.match";
groupId = "org.clojure";
sha512 = "52ada3bbe73ed1b429be811d3990df0cdb3e9d50f2a6c92b70d490a8ea922d4794da93c3b7487653f801954fc599704599b318b4d7926a9594583df37c55e926";
version = "1.0.0";
};
paths = [ src ];
}
rec {
name = "tools.reader/org.clojure";
src = fetchMavenArtifact {
inherit repos;
artifactId = "tools.reader";
groupId = "org.clojure";
sha512 = "3481259c7a1eac719db2921e60173686726a0c2b65879d51a64d516a37f6120db8ffbb74b8bd273404285d7b25143ab5c7ced37e7c0eaf4ab1e44586ccd3c651";
version = "1.3.6";
};
paths = [ src ];
}
rec {
name = "core.memoize/org.clojure";
src = fetchMavenArtifact {
inherit repos;
artifactId = "core.memoize";
groupId = "org.clojure";
sha512 = "67196537084b7cc34a01454d2a3b72de3fddce081b72d7a6dc1592d269a6c2728b79630bd2d52c1bf2d2f903c12add6f23df954c02ef8237f240d7394ccc3dde";
version = "1.0.253";
};
paths = [ src ];
}
rec {
name = "data.priority-map/org.clojure";
src = fetchMavenArtifact {
inherit repos;
artifactId = "data.priority-map";
groupId = "org.clojure";
sha512 = "bb8bc5dbfd3738c36b99a51880ac3f1381d6564e67601549ef5e7ae2b900e53cdcdfb8d0fa4bf32fb8ebc4de89d954bfa3ab7e8a1122bc34ee5073c7c707ac13";
version = "1.1.0";
};
paths = [ src ];
}
(rec {
name = "arachne-framework/valuehash";
src = fetchgit {
name = "valuehash";
url = "https://git.fudo.org/fudo-public/valuehash.git";
rev = "9d2dbafdb5db886a57f44c5b7fe32c824713e6c7";
sha256 = "1civ393c4yy9p2xbmrrvpbyqczx55k3fkvimkf850fl62ns5zl9r";
};
paths = map (path: src + path) [
"/src"
];
})
rec {
name = "core.cache/org.clojure";
src = fetchMavenArtifact {
inherit repos;
artifactId = "core.cache";
groupId = "org.clojure";
sha512 = "0a07ceffc2fa3a536b23773eefc7ef5e1108913b93c3a5416116a6566de76dd5c218f3fb0cc19415cbaa8843838de310b76282f20bf1fc3467006c9ec373667e";
version = "1.0.225";
};
paths = [ src ];
}
rec {
name = "core.async/org.clojure";
src = fetchMavenArtifact {
inherit repos;
artifactId = "core.async";
groupId = "org.clojure";
sha512 = "160a77da25382d7c257eee56cfe83538620576a331e025a2d672fc26d9f04e606666032395f3c2e26247c782544816a5862348f3a921b1ffffcd309c62ac64f5";
version = "1.5.648";
}; };
paths = [ src ]; paths = [ src ];

View File

@ -16,14 +16,15 @@
# defaultPackage = packages.${system}.worther; # defaultPackage = packages.${system}.worther;
# }; # };
outputs = { self, nixpkgs, flake-utils, ... }: with nixpkgs.lib; outputs = { self, nixpkgs, flake-utils, ... }:
with nixpkgs.lib;
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let pkgs = import nixpkgs { inherit system; };
pkgs = import nixpkgs { inherit system; };
worther = pkgs.callPackage ./worther.nix { pkgs = pkgs; };
in rec { in {
packages.worther = worther; packages = rec {
defaultPackage = self.packages.${system}.worther; default = worther;
worther = pkgs.callPackage ./worther.nix { pkgs = pkgs; };
};
}); });
} }

View File

@ -1,5 +1,101 @@
(ns worther.core) (ns worther.core
(:require [worther.injest.bittrex :as bittrex]
[worther.injest.coinbase :as cb]
[worther.injest.coinbase-pro :as cb-pro]
[worther.txn :as txn]
[clojure.pprint :refer [pprint]]
[clojure.string :as str]
[clojure.tools.cli :refer [parse-opts]])
(:gen-class))
(defn- ensure-directory [dir]
(if (not (.isDirectory dir))
(throw (java.io.FileNotFoundException.
(str "Invalid directory: " dir)))
dir))
(defn- directory-contents [dir-str]
(-> dir-str clojure.java.io/file ensure-directory file-seq))
(defn- has-ext? [ext]
(fn [file]
(re-matches (re-pattern (str ".+" "\\." ext "$"))
(str file))))
(defn- pthru [obj]
(pprint obj)
obj)
(defn- load-dir-csvs [loader dir]
(mapcat loader
(filter (has-ext? "csv")
(directory-contents dir))))
(def bittrex-load (partial load-dir-csvs bittrex/load-csv))
(def coinbase-load (partial load-dir-csvs cb/load-csv))
(def coinbase-pro-load (partial load-dir-csvs cb-pro/load-csv))
(defn- directory? [dir]
(-> dir clojure.java.io/file .isDirectory))
(defn- valid-year? [year]
(< 2010 year 2100))
(def cli-options
[["-h" "--help"]
["-c" "--coinbase DIR" "Coinbase CSV directory"
:default nil
:parse-fn clojure.java.io/file
:validate [directory? "Must be a valid directory."]]
["-C" "--coinbase-pro DIR" "Coinbase Pro CSV directory"
:default nil
:parse-fn clojure.java.io/file
:validate [directory? "Must be a valid directory."]]
["-b" "--bittrex DIR" "Bittrex CSV directory"
:default nil
:parse-fn clojure.java.io/file
:validate [directory? "Must be a valid directory."]]
["-y" "--year YEAR" "Year for which to generate tax data."
:parse-fn #(Integer/parseInt %)
:validate [valid-year? "Must be a year between 2010 and 2100."]]])
(defn- print-usage [opts]
(->> ["usage: worther [options]" "" "options:" opts]
(str/join \newline)))
(defn- error? [status]
(not (= 0 status)))
(defn- error-quit [status msg]
(if (error? status)
(println (str "error: " msg))
(println msg))
(System/exit status))
(def join-lines (partial str/join \newline))
(defmacro launch-task [name body]
(let [res (gensym)]
`(do (println (str "beginning " ~name "..."))
(let [~res ~body]
(println (str "completed " ~name "."))
~res))))
(defn- load-data [opts]
(let [before (fn [a b] (.before a b))]
(sort-by ::txn/timestamp
before
(concat (or (some-> opts :coinbase coinbase-load) [])
(or (some-> opts :coinbase-pro coinbase-pro-load) [])
(or (some-> opts :bittrex bittrex-load) [])))))
(defn -main [& args] (defn -main [& args]
(println "Not implemented yet!") (let [opts (parse-opts args cli-options)]
(println (str "called with: " args))) (cond (-> opts :options :help) (->> opts :summary print-usage (error-quit 0))
(-> opts :errors empty? not) (->> opts :errors join-lines (error-quit 1))
:else (println (str "count: " (count (load-data (:options opts))))))))

View File

@ -1,30 +1,32 @@
(ns worther.injest.bittrex (ns worther.injest.bittrex
(:require [worther.injest.common :as common] (:require [worther.injest.common :as common]
[worther.txn :as txn]
[clojure.core.match :refer [match]] [clojure.core.match :refer [match]]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.string :as str])) [clojure.string :as str]))
(defn split-buy [txn] (defn split-buy [txn]
[(-> txn [(-> txn
(assoc ::common/txn-type ::common/buy (assoc ::txn/txn-type ::txn/buy
::common/quantity-transacted (::quantity txn) ::txn/quantity-transacted (::quantity txn)
::common/currency (::market-currency txn))) ::txn/currency (::market-currency txn)))
(-> txn (-> txn
(assoc ::common/txn-type ::common/sell (assoc ::txn/txn-type ::txn/sell
::common/quantity-transacted (- (* (::price-per txn) ::txn/quantity-transacted (- (* (::price-per txn)
(::quantity txn))) (::quantity txn)))
::common/currency (::base-currency txn)))]) ::txn/currency (::base-currency txn)))])
(defn split-sell [txn] (defn split-sell [txn]
[(-> txn [(-> txn
(assoc ::common/txn-type ::common/sell (assoc ::txn/txn-type ::txn/sell
::common/quantity-transacted (- (::quantity txn)) ::txn/quantity-transacted (- (::quantity txn))
::common/currency (::market-currency txn))) ::txn/currency (::market-currency txn)))
(-> txn (-> txn
(assoc ::common/txn-type ::common/buy (assoc ::txn/txn-type ::txn/buy
::common/quantity-transacted (* (::price-per txn) ::txn/quantity-transacted (* (::price-per txn)
(::quantity txn)) (::quantity txn))
::common/currency (::base-currency txn)))]) ::txn/currency (::base-currency txn)))])
(defmulti type-specific-translation ::txn-type) (defmulti type-specific-translation ::txn-type)
@ -41,10 +43,10 @@
(split-buy txn)) (split-buy txn))
(defmethod type-specific-translation :fee [txn] (defmethod type-specific-translation :fee [txn]
[(assoc txn ::common/txn-type ::common/fee)]) [(assoc txn ::txn/txn-type ::txn/fee)])
(defmethod type-specific-translation nil [txn] (defmethod type-specific-translation nil [txn]
(throw (RuntimeException. (str "::txn-type missing from transaction: " txn)))) (throw (RuntimeException. (str "::txn-type missing from transaction: " (common/pprint-str txn)))))
(defn to-uuid [str] (java.util.UUID/fromString str)) (defn to-uuid [str] (java.util.UUID/fromString str))
@ -52,11 +54,11 @@
[txn [txn
{ {
::txn-type :fee ::txn-type :fee
::common/timestamp (::common/timestamp txn) ::txn/timestamp (::txn/timestamp txn)
::common/account (::common/account txn) ::txn/account (::txn/account txn)
::common/usd-value :deferred ::txn/usd-value :deferred
::common/quantity-transacted (::quantity txn) ::txn/quantity-transacted (::quantity txn)
::common/currency (::base-currency txn) ::txn/currency (::base-currency txn)
}]) }])
(defn split-currencies [txn] (defn split-currencies [txn]
@ -74,7 +76,7 @@
split-currencies split-currencies
(common/alter-field :uuid to-uuid) (common/alter-field :uuid to-uuid)
(common/rename-field :timestamp (common/rename-field :timestamp
::common/timestamp ::txn/timestamp
(common/date-parser "M/d/yyyy HH:mm:ss aa")) (common/date-parser "M/d/yyyy HH:mm:ss aa"))
(common/rename-field :ordertype (common/rename-field :ordertype
::txn-type ::txn-type
@ -97,15 +99,17 @@
(common/rename-field :closed (common/rename-field :closed
::closed ::closed
(common/date-parser "M/d/yyyy HH:mm:ss aa")) (common/date-parser "M/d/yyyy HH:mm:ss aa"))
(common/add-field ::common/account (common/add-field ::txn/account
(fn [_] :bittrex)))) (fn [_] :bittrex))))
(defn load-csv [filename] (defn load-csv [filename]
(let [txn-translator (common/flatcomp (let [txn-translator (common/flatcomp
(common/add-txid ::common/txid) (common/add-txid ::txn/txid)
type-specific-translation type-specific-translation
translate-bittrex-txn)] translate-bittrex-txn)]
(->> (common/load-csv filename) (->> (common/load-csv filename)
(common/pivot-csv) (common/pivot-csv)
(common/flatmap txn-translator)))) (common/flatmap txn-translator))))
(s/fdef load-csv
:args (s/cat :filename string?)
:ret (s/coll-of ::txn/txn))

View File

@ -1,6 +1,9 @@
(ns worther.injest.coinbase (ns worther.injest.coinbase
(:require [worther.injest.common :as common] (:require [worther.injest.common :as common]
[clojure.spec.alpha :as s])) [worther.txn :as txn]
[clojure.spec.alpha :as s]
[clojure.string :as str]
[clojure.pprint :refer [pprint]]))
(defn print-through [o] (defn print-through [o]
(clojure.pprint/pprint o) (clojure.pprint/pprint o)
@ -13,90 +16,115 @@
(def txn-type-map (def txn-type-map
{ {
:fee ::common/fee :fee ::txn/fee
:buy ::common/buy :buy ::txn/buy
:sell ::common/sell :sell ::txn/sell
:send ::common/send :send ::txn/send
:receive ::common/receive :receive ::txn/receive
:paid-for-an-order ::common/sell :paid-for-an-order ::txn/sell
:cardspend ::txn/sell
:cardbuyback ::txn/buy
:rewards-income ::txn/income
:convert ::txn/sell
}) })
(s/def ::txn-type (set (keys txn-type-map))) (s/def ::txn-type (set (keys txn-type-map)))
(defn update-txn-type [txn] (defn update-txn-type [txn]
(assoc txn ::common/txn-type (assoc txn ::txn/txn-type
(get txn-type-map (::txn-type txn)))) (get txn-type-map (::txn-type txn))))
(defmulti type-specific-translation ::txn-type) (defmulti type-specific-translation ::txn-type)
(s/fdef type-specific-translation
:args (s/cat :txn ::coinbase-txn)
:ret (s/+ ::txn/txn))
(defn split-fee [txn] (defn split-fee [txn]
(if (and (::usd-fees txn) (if (> (::usd-fees txn) 0)
(> (::usd-fees txn) 0))
[(update-txn-type txn) [(update-txn-type txn)
{ {
::txn-type :fee ::txn-type :fee
::common/quantity-transacted (::usd-fees txn) ::txn/txn-type ::txn/fee
::common/currency :usd ::txn/quantity-transacted (::usd-fees txn)
::common/account :coinbase ::txn/currency :usd
::common/timestamp (::common/timestamp txn) ::txn/account :coinbase
::common/notes (str "Fee for: " (::common/notes txn)) ::txn/timestamp (::txn/timestamp txn)
::common/usd-value (::usd-fees txn) ::txn/notes (str "Fee for: " (::txn/notes txn))
::txn/usd-value (::usd-fees txn)
}] }]
[(update-txn-type txn)])) [(update-txn-type txn)]))
(defmethod type-specific-translation :buy [txn] (defn- extract-conversion-data [conv-str]
[(update-txn-type txn) (let [read-amount (fn [amt]
{ (-> amt
::common/txn-type ::common/deposit (str/replace #"," "")
::common/quantity-transacted (::common/usd-value txn) (bigdec)))
::common/currency :usd number-rx "[0-9]+(,[0-9]+)*\\.[0-9]+"
::common/account :coinbase currency-rx "[A-Z][A-Z0-9]{2,5}"
::common/timestamp (::common/timestamp txn) conversion-rx (re-pattern (str "^Converted "
::common/notes (str "Deposit for: " (::common/notes txn)) number-rx
::common/usd-value (::common/usd-value txn) " "
}]) currency-rx
(defmethod type-specific-translation :sell [txn] " to (?<amt>"
[(update (update-txn-type txn) ::common/quantity-transacted -) number-rx
{ ") (?<currency>"
::common/txn-type ::common/withdrawal currency-rx
::common/quantity-transacted (::common/usd-value txn) ")$"))
::common/currency :usd matches (re-matches conversion-rx conv-str)
::common/account :coinbase amt (-> matches (nth 2) read-amount)
::common/timestamp (::common/timestamp txn) currency (-> matches (nth 4) common/to-keyword)]
::common/notes (str "Withdrawal for: " (::common/notes txn)) [amt currency]))
::common/usd-value (::common/usd-value txn)
}])
(defmethod type-specific-translation :paid-for-an-order [txn] (defmethod type-specific-translation :convert [txn]
(let [usd-value (* (::common/usd-current-price txn) (let [[qty currency] (-> txn ::txn/notes extract-conversion-data)]
(::common/quantity-transacted txn))] [(-> txn
[(-> (update-txn-type txn) update-txn-type
(assoc ::common/usd-value (- usd-value))) (update ::txn/quantity-transacted (comp - abs)))
{ {
::common/txn-type ::common/send ::txn-type :buy
::common/quantity-transacted usd-value ::txn/txn-type ::txn/buy
::common/currency :usd ::txn/quantity-transacted qty
::common/account :coinbase ::txn/currency currency
::common/timestamp (::common/timestamp txn) ::txn/account :coinbase
::common/notes (str "Send to merchant for: " (::common/notes txn)) ::txn/timestamp (::txn/timestamp txn)
::common/usd-value usd-value ::txn/notes (str "Converted from " (::txn/currency txn) " to " currency)
::common/recipient :merchant ::txn/usd-value :deferred
}])) }]))
(defmethod type-specific-translation :buy [txn]
[(-> txn update-txn-type)])
(defmethod type-specific-translation :cardbuyback [txn]
[(-> txn update-txn-type)])
(defmethod type-specific-translation :sell [txn]
[(-> txn update-txn-type
(update ::txn/quantity-transacted (comp - abs)))])
(defmethod type-specific-translation :paid-for-an-order [txn]
(let [usd-value (* (::txn/usd-current-price txn)
(::txn/quantity-transacted txn))]
[(-> txn update-txn-type (assoc ::txn/usd-value (- usd-value)))]))
(defmethod type-specific-translation :cardspend [txn]
(let [usd-value (* (::txn/usd-current-price txn)
(::txn/quantity-transacted txn))]
[(-> txn update-txn-type (assoc ::txn/usd-value (- usd-value)))]))
(defmethod type-specific-translation :send [txn] (defmethod type-specific-translation :send [txn]
(let [usd-value (* (::common/usd-current-price txn) (let [usd-value (* (::txn/usd-current-price txn)
(::common/quantity-transacted txn))] (::txn/quantity-transacted txn))]
[(-> (update-txn-type txn) [(-> (update-txn-type txn)
(assoc ::common/recipient :unknown) (assoc ::txn/recipient :unknown)
(assoc ::common/usd-value usd-value) (assoc ::txn/usd-value usd-value)
(update ::common/quantity-transacted -))])) (update ::txn/quantity-transacted -))]))
(defmethod type-specific-translation :receive [txn] (defmethod type-specific-translation :receive [txn]
(let [usd-value (* (::common/usd-current-price txn) (let [usd-value (* (::txn/usd-current-price txn)
(::common/quantity-transacted txn))] (::txn/quantity-transacted txn))]
[(-> (update-txn-type txn) [(-> (update-txn-type txn)
(assoc ::common/usd-value usd-value) (assoc ::txn/usd-value usd-value)
(assoc ::common/sender :unknown))])) (assoc ::txn/sender :unknown))]))
(defmethod type-specific-translation :fee [txn] (defmethod type-specific-translation :fee [txn]
[(update-txn-type txn)]) [(update-txn-type txn)])
@ -104,42 +132,57 @@
(defmethod type-specific-translation nil [txn] (defmethod type-specific-translation nil [txn]
(throw (RuntimeException. (str "Missing txn-type: " txn)))) (throw (RuntimeException. (str "Missing txn-type: " txn))))
(defn pthru [obj] (pprint obj) obj)
(defn- register-reward
"Coinbase Card pays rewards in XLM."
[txn]
(if (and (= (::txn/currency txn) :xlm)
(= (::txn-type txn) :receive))
[(-> txn
(assoc ::txn/txn-type ::txn/income)
(assoc ::txn/sender :coinbase-card))]
[txn]))
(def translate-coinbase-txns (def translate-coinbase-txns
(common/flatcomp (common/flatcomp
(common/add-field ::common/account (common/add-field ::txn/account
(fn [_] :coinbase)) (fn [_] :coinbase))
(common/rename-field :notes (common/rename-field :notes
::common/notes) ::txn/notes)
(common/rename-field :asset (common/rename-field :asset
::common/currency ::txn/currency
common/to-keyword) common/to-keyword)
(common/rename-field :usd-fees (common/rename-field :fees
::usd-fees ::usd-fees
(common/zero-or bigdec)) (common/zero-or bigdec))
(common/rename-field :usd-subtotal (common/rename-field :subtotal
::common/usd-value ::txn/usd-value
(common/null-or bigdec)) (common/null-or bigdec))
(common/rename-field :timestamp (common/rename-field :timestamp
::common/timestamp ::txn/timestamp
(common/date-parser "yyyy-MM-dd'T'HH:mm:ss'Z'")) (common/date-parser "yyyy-MM-dd'T'HH:mm:ss'Z'"))
(common/rename-field :transaction-type (common/rename-field :transaction-type
::txn-type ::txn-type
common/to-keyword) common/to-keyword)
(common/rename-field :usd-spot-price-at-transaction (common/rename-field :spot-price-at-transaction
::common/usd-current-price ::txn/usd-current-price
(common/null-or bigdec)) (common/null-or bigdec))
(common/rename-field :quantity-transacted (common/rename-field :quantity-transacted
::common/quantity-transacted ::txn/quantity-transacted
bigdec))) bigdec)))
(defn load-csv [filename] (defn load-csv [filename]
(let [header-line? (fn [cells] (= (first cells) "Timestamp")) (let [header-line? (fn [cells] (= (first cells) "Timestamp"))
txn-translator (common/flatcomp txn-translator (common/flatcomp (common/add-txid ::txn/txid)
(common/add-txid ::common/txid) register-reward
type-specific-translation type-specific-translation
split-fee split-fee
translate-coinbase-txns)] translate-coinbase-txns)]
(->> (common/load-csv filename) (->> (common/load-csv filename)
(drop-while (comp not header-line?)) (drop-while (comp not header-line?))
(common/pivot-csv) (common/pivot-csv)
(common/flatmap txn-translator)))) (common/flatmap txn-translator))))
(s/fdef load-csv
:args (s/cat :filename string?)
:ret (s/coll-of ::txn/txn))

View File

@ -1,5 +1,6 @@
(ns worther.injest.coinbase-pro (ns worther.injest.coinbase-pro
(:require [worther.injest.common :as common] (:require [worther.injest.common :as common]
[worther.txn :as txn]
[clojure.core.match :refer [match]] [clojure.core.match :refer [match]]
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]))
@ -30,62 +31,63 @@
(defn parse-int [i] (Integer/parseInt i)) (defn parse-int [i] (Integer/parseInt i))
(defn parse-uuid [str] (java.util.UUID/fromString str)) ;; Supplied by Clojure as of 1.11
;; (defn parse-uuid [str] (java.util.UUID/fromString str))
(defn print-through [o] (clojure.pprint/pprint o) o) (defn print-through [o] (clojure.pprint/pprint o) o)
(defn coinbase-pro-txn->common-txn [txn] (defn coinbase-pro-txn->common-txn [txn]
(get { (get {
:withdrawal ::common/withdrawal :withdrawal ::txn/withdrawal
:deposit ::common/deposit :deposit ::txn/deposit
} }
(::txn-type txn))) (::txn-type txn)))
(defn merge-match-order [spend receive common] (defn merge-match-order [spend receive common]
(match (map ::common/currency [spend receive]) (match (map ::txn/currency [spend receive])
[:usd _] [(merge common [:usd _] [(merge common
{ {
::common/txn-type ::common/buy ::txn/txn-type ::txn/buy
::common/usd-value (::common/quantity-transacted spend) ::txn/usd-value (::txn/quantity-transacted spend)
::common/quantity-transacted (::common/quantity-transacted receive) ::txn/quantity-transacted (::txn/quantity-transacted receive)
::common/currency (::common/currency receive) ::txn/currency (::txn/currency receive)
})] })]
[_ :usd] [(merge common [_ :usd] [(merge common
{ {
::common/txn-type ::common/sell ::txn/txn-type ::txn/sell
::common/usd-value (::common/quantity-transacted receive) ::txn/usd-value (::txn/quantity-transacted receive)
::common/quantity-transacted (::common/quantity-transacted spend) ::txn/quantity-transacted (::txn/quantity-transacted spend)
::common/currency (::common/currency spend) ::txn/currency (::txn/currency spend)
})] })]
:else [(merge common :else [(merge common
{ {
::common/txn-type ::common/sell ::txn/txn-type ::txn/sell
::common/usd-value :deferred ::txn/usd-value :deferred
::common/quantity-transacted (::common/quantity-transacted spend) ::txn/quantity-transacted (::txn/quantity-transacted spend)
::common/currency (::common/currency spend) ::txn/currency (::txn/currency spend)
}) })
(merge common (merge common
{ {
::common/txn-type ::common/buy ::txn/txn-type ::txn/buy
::common/usd-value :deferred ::txn/usd-value :deferred
::common/quantity-transacted (::common/quantity-transacted receive) ::txn/quantity-transacted (::txn/quantity-transacted receive)
::common/currency (::common/currency receive) ::txn/currency (::txn/currency receive)
})])) })]))
(def process-transfer (def process-transfer
(common/flatcomp (common/flatcomp
(common/add-field ::common/txn-type coinbase-pro-txn->common-txn))) (common/add-field ::txn/txn-type coinbase-pro-txn->common-txn)))
(defn process-match-order [[trade-id elems]] (defn process-match-order [[trade-id elems]]
(let [txn0 (first elems) (let [txn0 (first elems)
fee? (*-> ::txn-type (= :fee)) fee? (*-> ::txn-type (= :fee))
spend? (and-> (*-> ::common/quantity-transacted neg?) (*-> ::txn-type (= :match))) spend? (and-> (*-> ::txn/quantity-transacted neg?) (*-> ::txn-type (= :match)))
receive? (and-> (comp not neg? ::common/quantity-transacted) (*-> ::txn-type (= :match))) receive? (and-> (comp not neg? ::txn/quantity-transacted) (*-> ::txn-type (= :match)))
common-fields (select-keys txn0 common-fields (select-keys txn0
[::order-id [::order-id
::trade-id ::trade-id
::common/account ::txn/account
::common/timestamp ::txn/timestamp
::transfer-id ::transfer-id
::balance ::balance
::portfolio])] ::portfolio])]
@ -93,9 +95,9 @@
(find-first receive? elems) (find-first receive? elems)
common-fields) common-fields)
(map (fn [txn] (map (fn [txn]
(-> (select-keys txn [::common/quantity-transacted ::common/currency]) (-> (select-keys txn [::txn/quantity-transacted ::txn/currency])
(merge common-fields) (merge common-fields)
(assoc ::common/txn-type ::common/fee))) (assoc ::txn/txn-type ::txn/fee)))
(filter fee? elems))))) (filter fee? elems)))))
(defn group-transactions [txns] (defn group-transactions [txns]
@ -107,13 +109,13 @@
(def preclean-txns (def preclean-txns
(common/flatcomp (common/flatcomp
(common/rename-field :amount (common/rename-field :amount
::common/quantity-transacted ::txn/quantity-transacted
bigdec) bigdec)
(common/rename-field :amount/balance-unit (common/rename-field :amount/balance-unit
::common/currency ::txn/currency
common/to-keyword) common/to-keyword)
(common/rename-field :time (common/rename-field :time
::common/timestamp ::txn/timestamp
(common/date-parser "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")) (common/date-parser "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"))
(common/rename-field :trade-id (common/rename-field :trade-id
::trade-id ::trade-id
@ -137,7 +139,7 @@
(defn load-csv [filename] (defn load-csv [filename]
(->> (common/load-csv filename) (->> (common/load-csv filename)
(common/pivot-csv) (common/pivot-csv)
(map #(assoc % ::common/account :coinbase-pro)) (map #(assoc % ::txn/account :coinbase-pro))
(common/flatmap preclean-txns) (common/flatmap preclean-txns)
(group-transactions) (group-transactions)
(common/flatmap (common/add-txid ::common/txid)))) (common/flatmap (common/add-txid ::txn/txid))))

View File

@ -3,7 +3,9 @@
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.string :as str] [clojure.string :as str]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[valuehash.api :as hash])) [valuehash.api :as hash]
[worther.txn :as txn]))
(s/def ::header keyword?) (s/def ::header keyword?)
@ -54,11 +56,11 @@
(defn generate-id [entry] (defn generate-id [entry]
;; Use a vector because I'm not sure the map will be ordered ;; Use a vector because I'm not sure the map will be ordered
(let [make-entry-vector (juxt ::timestamp (let [make-entry-vector (juxt :txn/timestamp
::quantity-transacted :txn/quantity-transacted
::txn-type :txn/txn-type
::account :txn/account
::currency)] :txn/currency)]
(-> entry (-> entry
(make-entry-vector) (make-entry-vector)
(hash/sha-256-str)))) (hash/sha-256-str))))
@ -70,7 +72,7 @@
([field new-field] (rename-field field new-field identity)) ([field new-field] (rename-field field new-field identity))
([field new-field f] (fn [entry] ([field new-field f] (fn [entry]
(when (not (contains? entry field)) (when (not (contains? entry field))
(throw (RuntimeException. (str "Field not found: " field)))) (throw (RuntimeException. (str "Field not found: " field " in " entry))))
[(assoc (dissoc entry field) [(assoc (dissoc entry field)
new-field new-field
(f (get entry field)))]))) (f (get entry field)))])))
@ -95,60 +97,22 @@
(defn zero-or [f] (defn zero-or [f]
(fn [str] (if (or (nil? str) (empty? str)) (bigdec 0) (f str)))) (fn [str] (if (or (nil? str) (empty? str)) (bigdec 0) (f str))))
(s/def ::txn-type #{::buy ::sell ::send ::receive ::deposit ::withdrawal ::fee ::income})
(defn valid-timestamp? [ts] (instance? java.util.Date ts))
(s/def ::timestamp valid-timestamp?)
(defn bigdec? [i] (instance? java.math.BigDecimal i))
(s/def ::quantity-transacted bigdec?)
(s/def ::usd-value (s/or :deferred #(= % :deferred)
:number bigdec?))
(s/def ::currency keyword?)
(s/def ::account keyword?)
(s/def ::sender keyword?)
(s/def ::recipient keyword?)
(defmulti txn ::txn-type)
(s/def ::txn (s/multi-spec txn ::txn-type))
(let [txn-common-req [::txid
::quantity-transacted
::currency
::txn-type
::account
::timestamp]]
(defmethod txn ::buy [_]
(s/keys :req (concat txn-common-req [::usd-value])))
(defmethod txn ::sell [_]
(s/keys :req (concat txn-common-req [::usd-value])))
(defmethod txn ::send [_]
(s/keys :req (concat txn-common-req [::recipient])))
(defmethod txn ::receive [_]
(s/keys :req (concat txn-common-req [::sender])))
(defmethod txn ::fee [_]
(s/keys :req (concat txn-common-req)))
(defmethod txn ::income [_]
(s/keys :req (concat txn-common-req [::src])))
(defmethod txn ::deposit [_]
(s/keys :req (concat txn-common-req [::src])))
(defmethod txn ::withdrawal [_]
(s/keys :req (concat txn-common-req [::dest]))))
(defn map-select [m] (defn map-select [m]
(fn [om] (fn [om]
(every? (fn [key] (= (get om key) (every? (fn [key] (= (get om key)
(get m key))) (get m key)))
(keys m)))) (keys m))))
(defn add-tag [tag src obj]
(with-meta obj { tag src }))
(def add-source-tag (partial add-tag :source))
(defn matching-files [rx dir] (defn matching-files [rx dir]
(filter (fn [file] (filter (fn [file]
(re-matches rx file)) (re-matches rx file))
(map str (file-seq (clojure.java.io/file dir))))) (map str (file-seq (clojure.java.io/file dir)))))
(defn pprint-str [obj]
(with-out-str (clojure.pprint/pprint obj))
obj)

158
src/worther/originator.clj Normal file
View File

@ -0,0 +1,158 @@
(ns worther.originator
(:require [worther.txn :as txn]
[clojure.spec.alpha :as s]))
;; Think this through.
;;
;; - The input will be a list of transactions. Step one: extract the sales.
;; - Order chronologically. These need mapping to purchases. Step two: extract
;; the purchases. Order chronologically, split by currency. Step three:
;; starting with the first sale, start consuming purchases until the total of
;; the sale is covered.
;; - Return: [ filled sales, remaining sales, remaining purchases ].
;; - In the end, return only filled sales.
(defn- positive? [n] (>= 0))
(s/def ::remaining (comp positive? number?))
(s/def ::contribution-amount (comp positive? number?))
(s/def ::contributions (s/coll-of (s/keys :req [::amount ::txn])))
(defn- sort-by-date [txns]
(sort (fn [a b] (.before a b)) txns))
(defn- normalize [i] (.setScale (bigdec i) 6 java.math.RoundingMode/HALF_UP))
(defn- txn-sourced? [txn]
(let [sum-coll (apply +)
src-amount (-> txn
::contributions
(map ::contribution-amount)
(sum-coll))]
(.equals (normalize (::amount txn))
(normalize src-amount))))
(defn- txn-consumed? [txn]
(.equals (normalize 0) (-> txn ::remaining normalize)))
(s/def ::contributions
(s/keys :req [::src-txn ::contribution-amount]))
(s/def ::sourced-txn
(s/and txn-sourced?
::sink-txn
(s/keys :req [::contributions])))
(defn originate-txn [sink srcs]
(let [src (first srcs)]
(if (>= (::remaining src) (::remaining sink))
;; Sink is fully covered
(let [contribution (::remaining sink)]
[(-> sink
(update ::remaining 0)
(update ::contributions conj
{
::contribution-amount contribution
::src-txn src
}))
(conj (rest srcs)
(update src ::remaining - contribution))])
;; Sink is not yet fully covered
(let [contribution (::remaining src)]
(originate-txn (-> sink
(update ::remaining - contribution)
(update ::contributions conj
{
::contribuution-amount contribution
::src-txn src
}))
(rest srcs))))))
(s/fdef originate-txn
:args (s/cat :sink ::sink-txn
:srcs (s/coll-of ::src-txn))
:ret ::sourced-txn)
(s/def ::txn-with-remaining
(s/and :txn/txn
(s/keys :req [::remaining])))
(defn- originate-txns-exec
"Given a sorted list of sink transactions and a sorted list of src transactions,
return a list of sinks mapped to originating src transactions."
[sinks srcs]
(loop [sinks sinks
srcs srcs
filled-sinks []]
(if (empty? sinks)
filled-sinks
(let [[filled-sink remaining-srcs]
(originate-txn (first sinks) srcs)]
(recur (rest sinks)
remaining-srcs
(conj filled-sinks filled-sink))))))
(s/fdef originate-txns-exec
:args (s/cat :sinks (s/coll-of (s/and :txn/sink-txn ::txn-with-remaining))
:srcs (s/coll-of (s/and :txn/src-txn ::txn-with-remaining)))
:ret (s/coll-of ::sourced-txn))
(defmulti inject-remaining
"Add a ::remaining key, containing the unallocated amount."
:txn/txn-type)
(s/fdef inject-remaining
:args (s/cat :txn :txn/txn)
:ret ::txn-with-remaining)
(defn- remaining-from [txn key]
(assoc txn ::remaining (get txn key)))
(s/fdef remaining-from
:args (s/cat :txn :txn/txn
:key keyword?)
:ret ::txn-with-remaining)
(defmethod inject-remaining :default [txn]
(remaining-from txn :txn/amount))
;; In most cases, simple: map every sink to sources for every account. But in
;; the case of a receive source, we need to look at the original account and
;; start finding sources there. They can't be shared.
;; TODO: inject ::remaining
#_(defn originate-txn [all-txns]
(let [srcs (group-by ::txn/account (filter txn/src-txn? all-txns))
sinks (filter txn/sink-txn? all-txns)]
{
:filled-sales []
:remaining-sales remaining-sales
:remaining-buys remaining-buys
}))
(defn- consume-src [sink src]
(let [contribution (apply min (map ::remaining [sink src]))]
[(-> sink
(update ::remaining - contribution)
(update ::contributions conj
{
::contribution-amount contribution
::src-txn src
}))
(update src ::remaining - contribution)]))
(s/fdef consume-src
:args (s/cat :sink ::txn/sink-txn
:src ::txn/src-txn)
:ret (s/cat :sink ::txn/sink-txn
:src ::txn/src-txn))
(s/def ::srcs-by-account (s/map-of ::txn/account (s/coll-of ::txn/src-txn)))
(defn originate-txn [sink srcs]
(let [sink-acct (::txn/account sink)
acct-srcs (get srcs sink-acct)]
(loop [])))
(s/fdef originate-txn
:args (s/cat :sink ::txn/sink-txn
:srcs ::srcs-by-account)
:ret (s/cat :sink ::sourced-txn
:srcs ::srcs-by-account))
(defn originate-txns [all-txns]
(group-by ::txn/account all-txns))

119
src/worther/txn.clj Normal file
View File

@ -0,0 +1,119 @@
(ns worther.txn
(:require [clojure.pprint :refer [pprint]]
[clojure.spec.alpha :as s]
[clojure.set :refer [union]]
[worther.txn :as txn]))
(def src-txn-types
#{
::buy
::receive
::deposit
::income
})
(def sink-txn-types
#{
::sell
::send
::withdrawal
::fee
})
(s/def ::txn-type (union src-txn-types sink-txn-types))
(s/def ::src-txn-type src-txn-types)
(s/def ::sink-txn-type sink-txn-types)
(defn src-txn? [txn] (->> txn ::txn-type (contains? src-txn-types)))
(defn sink-txn? [txn] (->> txn ::txn-type (contains? sink-txn-types)))
(defn valid-timestamp? [ts] (instance? java.util.Date ts))
(s/def ::timestamp valid-timestamp?)
(defn bigdec? [i] (instance? java.math.BigDecimal i))
(s/def ::quantity-transacted bigdec?)
(s/def ::usd-value (s/or :deferred #(= % :deferred)
:number bigdec?))
(s/def ::currency keyword?)
(s/def ::account keyword?)
(s/def ::sender keyword?)
(s/def ::recipient keyword?)
(defmulti txn ::txn-type)
(s/def ::txn (s/multi-spec txn ::txn-type))
(defn src-txn? [txn]
(->> txn
::txn-type
(contains? src-txn-types)))
(defn sink-txn? [txn]
(->> txn ::txn-type (contains? sink-txn-types)))
(s/def ::src-txn src-txn?)
(s/def ::sink-txn sink-txn?)
(defn- pprint-to-string [o]
(with-out-str (pprint o)))
(defn- pthru [obj]
(clojure.pprint/pprint obj)
obj)
(defn- negative? [key]
(fn [txn] (-> txn key (<= 0))))
(defn- positive? [key]
(fn [txn] (-> txn key (>= 0))))
(let [txn-common-req [::txid
::quantity-transacted
::currency
::txn-type
::account
::timestamp]]
(defmethod txn ::buy [_]
(s/and (s/keys :req (concat txn-common-req [::usd-value]))
;; deferred
;;(negative? ::usd-value)
(positive? ::quantity-transacted)))
(defmethod txn ::sell [_]
(s/and (s/keys :req (concat txn-common-req [::usd-value]))
(negative? ::quantity-transacted)
;; Deferred
;;(positive? ::usd-value)
))
(defmethod txn ::send [_]
(s/and (s/keys :req (concat txn-common-req [::recipient]))
(negative? ::quantity-transacted)))
(defmethod txn ::receive [_]
(s/and (s/keys :req (concat txn-common-req [::sender]))
(positive? ::quantity-transacted)))
(defmethod txn ::fee [_]
(s/and (s/keys :req (concat txn-common-req))
;; Deferred
;;(negative? ::usd-value)
))
(defmethod txn ::income [_]
(s/and (s/keys :req (concat txn-common-req [::src]))
(positive? ::quantity-transacted)))
(defmethod txn ::deposit [_]
(s/and (s/keys :req (concat txn-common-req [::src]))
(positive? ::quantity-transacted)))
(defmethod txn ::withdrawal [_]
(s/and (s/keys :req (concat txn-common-req [::dest]))
(negative? ::quantity-transacted)))
(defmethod txn :default [txn]
(throw (RuntimeException.
(str "No method defined for txn-type: "
(::txn-type txn)
"\n"
(pprint-to-string txn))))))