From 0aff915bb83c965c4e2d3f0595df7b119a9e2ebd Mon Sep 17 00:00:00 2001 From: Peter Selby Date: Sun, 7 Oct 2018 22:33:25 -0700 Subject: [PATCH] Why are buys mapping to sells? --- project.clj | 3 +- resources/bittrex-fullOrders.csv | 2 +- resources/taxes.org | 10 +- src/taxer/core.clj | 59 +++++++- src/taxer/executor.clj | 208 +++++++++++++++++++++++++++-- src/taxer/importer.clj | 222 ++++++++++++++++--------------- src/taxer/ops.clj | 34 ++++- 7 files changed, 408 insertions(+), 130 deletions(-) diff --git a/project.clj b/project.clj index 214cdc5..f7e946d 100644 --- a/project.clj +++ b/project.clj @@ -17,4 +17,5 @@ [org.clojure/data.json "0.2.6"] [orchestra "2018.08.19-1"] [org.fudo.utils "0.0.2"] - [org.clojure/core.match "0.3.0-alpha5"]]) + [org.clojure/core.match "0.3.0-alpha5"] + [org.clojure/test.check "0.9.0"]]) diff --git a/resources/bittrex-fullOrders.csv b/resources/bittrex-fullOrders.csv index fb5fa12..52c3a83 100644 --- a/resources/bittrex-fullOrders.csv +++ b/resources/bittrex-fullOrders.csv @@ -35,4 +35,4 @@ a5231ece-4c46-4a31-9e2e-b093d9cf6a8b,USDT-BTG,LIMIT_SELL,7.94203782,315.0000001, 7cce5777-ed16-4b96-9c60-95f70e975453,BTC-ADA,LIMIT_SELL,10000,0.0000275,0.00069251,0.27700857,12/17/2017 7:46:28 AM,12/17/2017 7:46:28 AM d49216c6-7513-4f8a-ab26-22a402a8f781,BTC-ADA,LIMIT_SELL,10000,0.00005363,0.00134074,0.53629998,12/17/2017 7:59:03 AM,12/31/2017 12:41:57 AM 95efafd5-5978-42ad-9f64-3caf5976310a,BTC-ADA,LIMIT_SELL,10000,0.00005225,0.00130625,0.5225,12/17/2017 7:58:50 AM,12/31/2017 12:40:05 AM -7fc00045-5a59-4315-86a5-a8a31080111d,BTC-ADA,LIMIT_SELL,10000,0.000044,0.0011,0.44,12/17/2017 7:48:02 AM,12/30/2017 7:32:42 AM \ No newline at end of file +7fc00045-5a59-4315-86a5-a8a31080111d,BTC-ADA,LIMIT_SELL,10000,0.000044,0.0011,0.44,12/17/2017 7:48:02 AM,12/30/2017 7:32:42 AM diff --git a/resources/taxes.org b/resources/taxes.org index 82e00a0..73d3a24 100644 --- a/resources/taxes.org +++ b/resources/taxes.org @@ -12,6 +12,7 @@ Basic idea: - BCH and BTG count as income: all deposits should be multiplied by the day-1 value and counted as income--everything else counts as capital gains. - BCH traded on the first day at $277, according to bitcoin.tax. + - Apparently BTG opened at $479! Huh. - Uhh, some Coinbase "sends" are really sales. Look for "Sent to Coinbase". - Coinbase sent to Vault of Satoshi, where they were sold. Should be considered sold...but I don't know how to calculate the tax. Anyway, I @@ -22,10 +23,17 @@ Basic idea: - 18as544Wxg3ZScCZo7fjWSV4JGUhsLv6AR - 18as544Wxg3ZScCZo7fjWSV4JGUhsLv6AR - 1GxxcLKjqHD1wZWoZ3KNgiqjQonGDpZS4 - - 1G3GM7izNsegWnxTB3RbuXAkC9YZxfeYP1 - Definitely gifts: - 1L4kschshGtKJPM3T5RXhBJYtEHhFa3xq (Omar) - 18Co5639x3Dp1EExfbgEzBKXYn329cwYKt + - 17XNrTEqMe4PkUcCFVaihW7Yu2gFasaKxM + - 15My27F2QLkLrZ3bqR8SSUHHrCKwQyGeLd + - 1Ae5kmNpAweTDGGguQCK55G84LDcHArJYn + - 1G3GM7izNsegWnxTB3RbuXAkC9YZxfeYP1 + - 18Co5639x3Dp1EExfbgEzBKXYn329cwYKt + - Purchases:r + - 1EjPaprQpLmPPJnfECRg9jK9v7eyEYkBNH + - 1Nc59oJunufbRe6uhqByJ1Kop19tqTQ1hr - Vircurex: - 1FripmTRgNFx6M2C7udeDWKYW8wfR5vuUU - 1FripmTRgNFx6M2C7udeDWKYW8wfR5vuUU diff --git a/src/taxer/core.clj b/src/taxer/core.clj index 11ca7c1..2ea5a9c 100644 --- a/src/taxer/core.clj +++ b/src/taxer/core.clj @@ -1,12 +1,17 @@ (ns taxer.core - (:require [clojure.spec.alpha :as s])) + (:require [clojure.spec.alpha :as s] + [clojure.spec.gen.alpha :as gen])) (s/def ::currency #{:usd :bch :btc :eth :ada :btg}) (s/def ::from-currency ::currency) (s/def ::to-currency ::currency) -(s/def ::datetime (fn [obj] (instance? java.util.Date obj))) +(def datetime-gen (gen/fmap #(java.util.Date.) + (s/gen (s/int-in 0 4133923200000)))) + + +(s/def ::datetime (s/inst-in #inst "1990" #inst "2100")) (s/def ::timestamp integer?) @@ -15,7 +20,7 @@ (re-matches #"^[0-9a-f]{40}$" obj)))) (s/def ::id ::sha1-sum) -(s/def ::txn-type #{:buy :sell :send :receive :trade :fee}) +(s/def ::txn-type #{:buy :sell :send :trade :fee :income :deposit}) (s/def ::from-account ::sha1-sum) (s/def ::to-account ::sha1-sum) @@ -29,7 +34,8 @@ ::currency ::txn-type ::account - ::currency]) + ::currency + ::datetime]) (defmulti txn-type ::txn-type) (defmethod txn-type :buy [_] (s/keys :req (concat txn-common-req @@ -52,10 +58,55 @@ (defmethod txn-type :fee [_] (s/keys :req (concat txn-common-req []) :opt [::notes])) +(defmethod txn-type :deposit [_] + (s/keys :req (concat txn-common-req []) + :opt [::notes])) (s/def ::txn (s/multi-spec txn-type ::txn-type)) (s/def ::txns (s/coll-of ::txn)) +(defn nonnegative-float? [obj] + (and (float? obj) (>= obj 0))) + +(defn positive-float? [obj] + (and (float? obj) (> obj 0))) + +(defn sell? [obj] + (= :sell (::txn-type obj))) + +(defn acquisition? [obj] + (or (= :income (::txn-type obj)) + (= :buy (::txn-type obj)))) + +(defn fully-sourced? [txn] + (< (* 0.001 (::amount txn)) + (Math/abs (- (reduce + (map ::amount (::consumed txn))) + (::amount txn))))) + +(s/def ::sell (s/and ::txn sell?)) +(s/def ::acquisition (s/and ::txn acquisition?)) + +(s/def ::consumed (s/coll-of (s/keys :amount positive-float? + :txn ::txn))) + +(s/def ::unconsumed nonnegative-float?) + +(s/def ::sourced-sell + (s/and (s/keys :req [::consumed]) + fully-sourced?)) +(s/def ::unsourced-sell (s/keys :req [::consumed])) +(s/def ::unconsumed-buy (s/keys :req [::unconsumed-amount])) + +(s/def ::open number?) +(s/def ::close number?) +(s/def ::high number?) +(s/def ::low number?) +(s/def ::volume-from number?) +(s/def ::volume-to number?) +(s/def ::from-currency ::currency) +(s/def ::to-currency ::currency) +(s/def ::date ::datetime) + (s/def ::pricemap (s/map-of ::timestamp (s/keys :req-un [::date diff --git a/src/taxer/executor.clj b/src/taxer/executor.clj index 132453c..e75d30b 100644 --- a/src/taxer/executor.clj +++ b/src/taxer/executor.clj @@ -1,8 +1,12 @@ (ns taxer.executor (:require [taxer.importer :as import] + [taxer.ops :as op] [taxer.core :as tax] [clojure.java.io :as io] - [clojure.spec.alpha :as s])) + [clojure.spec.alpha :as s] + [orchestra.spec.test :as st])) + +(st/instrument) (defn gdax [] (import/merge-gdax-transactions @@ -37,9 +41,6 @@ (import/merge-bittrex-transactions (rates) (import/load-bittrex-csv "resources/bittrex-fullOrders.csv"))) -(defn all-txns [] - (concat (gdax) (coinbase) (bittrex))) - (defn project [ks m] (into {} (map (fn [k] [k (get m k)]) ks))) @@ -53,19 +54,208 @@ (defn update-field [field f txns] (map (fn [txn] (update txn field f)) txns)) +(defn build-tx [amount currency timestamp value-per txn-type] + (import/common->local {::tax/amount amount + ::tax/currency currency + ::tax/datetime timestamp + ::tax/usd-amount (* value-per amount) + ::tax/txn-type txn-type + ::tax/txn-id "injected"})) + +(def mk-date (import/parse-date "y-M-d")) + +(defn bcc->bch [txns] + (map (fn [tx] + (if (= (::tax/currency tx) :bcc) + (assoc tx ::tax/currency :bch) + tx)) + txns)) + +(defn insert-magic-txns [txns] + (-> txns + ;; From the POV of taxes, these are income + (conj (build-tx 125.1234564 + :bch + (mk-date "2017-08-17") + 277 + :income)) + (conj (build-tx 125.1234564 + :btg + (mk-date "2017-11-12") + 479 + :income)) + ;; But they have to be discoverable to calculate capital gains + (conj (build-tx 125.1234564 + :bch + (mk-date "2017-08-17") + 277 + :buy)) + (conj (build-tx 125.1234564 + :btg + (mk-date "2017-11-12") + 479 + :buy)))) + +(defn modify-where [txns pred mod] + (map (fn [tx] (if (pred tx) (mod tx) tx)) txns)) + +(defn coinbase-sends-to-sells [txns] + (modify-where txns + (fn [tx] (re-matches #"^Sent to Coinbase" (or (::tax/notes tx) ""))) + (fn [tx] (assoc tx ::tax/txn-type :sell)))) + +(defn coinbase-switch-type [txns addresses type memo] + (modify-where txns + (fn [tx] + (some (fn [address] (.contains (or (::tax/notes tx) "") address)) + addresses)) + (fn [tx] (assoc tx + ::tax/txn-type type + ::tax/notes (format "%s - %s" + (::tax/notes tx) + memo))))) + +(defn coinbase-manual-modify [txns] + (-> txns + (coinbase-switch-type ["1AF6ZPez9NFc7nUfJtwBgod6aWcYaDTi3F" + "1LTrqFApTvfSn415Rjs1ukCVN149zADxxJ" + "1LTrqFApTvfSn415Rjs1ukCVN149zADxxJ" + "18as544Wxg3ZScCZo7fjWSV4JGUhsLv6AR" + "18as544Wxg3ZScCZo7fjWSV4JGUhsLv6AR" + "1GxxcLKjqHD1wZWoZ3KNgiqjQonGDpZS4"] + :sell + "Sent to Vault of Satoshi, sold from there, taxes paid previously") + (coinbase-switch-type ["1L4kschshGtKJPM3T5RXhBJYtEHhFa3xq" + "18Co5639x3Dp1EExfbgEzBKXYn329cwYKt" + "17XNrTEqMe4PkUcCFVaihW7Yu2gFasaKxM" + "15My27F2QLkLrZ3bqR8SSUHHrCKwQyGeLd" + "1Ae5kmNpAweTDGGguQCK55G84LDcHArJYn" + "1G3GM7izNsegWnxTB3RbuXAkC9YZxfeYP1" + "18Co5639x3Dp1EExfbgEzBKXYn329cwYKt"] + :sell + "Gifts or payments sent to friends & family") + (coinbase-switch-type ["1EjPaprQpLmPPJnfECRg9jK9v7eyEYkBNH" + "1Nc59oJunufbRe6uhqByJ1Kop19tqTQ1hr"] + :sell + "Purchases (eg. Steam)"))) + +(defn all-txns [] + (sort-by ::tax/datetime + (-> (concat (gdax) (coinbase) (bittrex)) + (bcc->bch) + (insert-magic-txns) + (coinbase-sends-to-sells) + (coinbase-manual-modify)))) + +(defn currencies [txns] + (distinct (map ::tax/currency txns))) + +(defn filter-currency [txns curr] + (sort-by ::tax/datetime + (op/filter-on-value txns ::tax/currency curr))) + +(defn all-sells [txns] + (sort-by ::tax/datetime + (op/filter-on-value txns ::tax/txn-type :sell))) + +(defn all-buys [txns] + (sort-by ::tax/datetime + (op/filter-on-value txns + (fn [tx] + (or (= (::tax/txn-type tx) :buy) + (= (::tax/txn-type tx) :income)))))) + +(defrecord ConsumedBuy [amount txn]) + +(defn sourced-balance [sell] + (reduce + 0 (map :amount (::tax/consumed sell)))) + +(defn unsourced-remaining [sell] + (- (::tax/amount sell) (sourced-balance sell))) + +;; Okay, what to do, what to do... +;; - Take a list of all buys and all sells +;; - Starting with the first sell, consume as many 'buys' as necessary to cover +;; - From each, subtract the relevant amount...returning both +;; - Move on to the next sell + +(defn consume-buy [sell buy] + (println (format "Taking %s (%s) from %s (%s)" + (unsourced-remaining sell) + (::tax/id sell) + (::tax/unconsumed-amount buy) + (::tax/id buy))) + (let [consumed-amount (min (unsourced-remaining sell) + (::tax/unconsumed-amount buy))] + (println (format "%s - consuming %s of %s, %s remaining" + (::tax/id sell) + consumed-amount + (unsourced-remaining sell) + (- (unsourced-remaining sell) consumed-amount))) + [(update sell ::tax/consumed + (fn [consumed-list] + (conj consumed-list (->ConsumedBuy consumed-amount buy)))) + (update buy ::tax/unconsumed-amount + (fn [amt] (- amt consumed-amount)))])) +(s/fdef consume-buy + :args (s/cat :sell ::tax/unsourced-sell :buy ::tax/unconsumed-buy) + :ret (s/cat :sell ::tax/sourced-sell :buy ::tax/consumed-buy)) + + +(defn consume-buys [sell unconsumed-buys] + (let [buy (first unconsumed-buys) + remaining-buys (rest unconsumed-buys) + [sourced-sell consumed-buy] (consume-buy sell buy)] + (if (<= 0 (unsourced-remaining sourced-sell)) + ;; We've sourced this full sell--return! + ;; Include the remainder of this buy, there's probably some left + [sourced-sell (cons consumed-buy remaining-buys)] + ;; There's a remaining balance that needs sourcing, and the current buy is + ;; spent--iterate! + (consume-buys sourced-sell remaining-buys)))) + + +(defn source-sells [raw-sells raw-buys] + "Given a list of sells and a list of buys, map sells to source buys." + (let [buys (map (fn [row] (assoc row ::tax/unconsumed-amount (::tax/amount row))) + raw-buys) + sells (map (fn [row] (assoc row ::tax/consumed [])) raw-sells)] + (loop [sell (first sells) + unsourced-sells (rest sells) + unconsumed-buys buys + sourced-sells []] + (if (empty? unsourced-sells) + (let [[sourced-sell _] (consume-buys sell unconsumed-buys)] + (conj sourced-sells sourced-sell)) + (let [[sourced-sell remaining-buys] (consume-buys sell unconsumed-buys)] + (recur (first unsourced-sells) + (rest unsourced-sells) + remaining-buys + (conj sourced-sells sourced-sell))))))) +(s/fdef source-sells + :args (s/cat :raw-sells ::tax/sell :raw-buys ::tax/acquisition) + :ret (s/coll-of ::tax/sourced-sell) + :fn #(and (= (-> % :args :raw-sells count) + (-> % :ret count)))) + +(defn source-sells-by-currency [txns currency] + (let [currency-txns (filter-currency txns currency)] + (source-sells (all-sells currency-txns) (all-buys currency-txns)))) + (defn print-as-table [txns] - (let [restrict-fields (partial project [::tax/timestamp - ::tax/txn-id + (let [restrict-fields (partial project [::tax/datetime + ;;::tax/txn-id ::tax/txn-type ::tax/amount ::tax/currency ::tax/usd-amount - ::tax/id - ;;::tax/notes + ;;::tax/account + ;;::tax/id + ::tax/notes ])] (clojure.pprint/print-table (->> txns - (sort-by ::tax/timestamp) + (sort-by ::tax/datetime) (update-field ::tax/amount round-str) (update-field ::tax/usd-amount round-str) (update-field ::tax/timestamp format-date) diff --git a/src/taxer/importer.clj b/src/taxer/importer.clj index 29a161e..cc762ff 100644 --- a/src/taxer/importer.clj +++ b/src/taxer/importer.clj @@ -1,5 +1,7 @@ + (ns taxer.importer (:require [taxer.core :as tax] + [taxer.ops :as op] [org.fudo.utils.sorted :as sort] [clojure.core.match :refer [match]] [clojure.data.csv :as csv] @@ -13,6 +15,9 @@ (defn file? [obj] (instance? java.io.File obj)) +(defn path-url? [obj] + (instance? java.net.URL obj)) + (s/def ::header keyword?) (defn headify [str] @@ -30,7 +35,7 @@ (s/fdef make-row :args (s/cat :headers (s/coll-of ::header) :fields (s/coll-of string?)) - :ret (s/map-of ::tax/header string?)) + :ret (s/map-of ::header string?)) (defn load-csv [file] (with-open [reader (io/reader file)] @@ -38,46 +43,27 @@ headers (map headify (first lines))] (map (partial make-row headers) (rest lines))))) (s/fdef load-csv - :args (s/cat :file file?) - :ret (s/coll-of (s/map-of ::tax/header string?))) - -(defn alter-field [field f] - (fn [row] - (if (get row field) - (update row field (fn [value] (f value))) - row))) - -(defn add-field [field f] - (fn [row] - (assoc row field (f row)))) - -(defn split-field [field header-generators & [passed-sep]] - (let [header-pairs (partition 2 header-generators) - sep (or passed-sep #" ")] - (fn [row] - (into row - (map (fn [[header generator] value] {header (generator value)}) - header-pairs - (str/split (field row) sep)))))) + :args (s/cat :file path-url?) + :ret (s/coll-of (s/map-of ::header string?))) (defn parse-date [date-format] (let [date-format (java.text.SimpleDateFormat. date-format)] (fn [date-str] (.parse date-format date-str)))) (defn load-coinbase-csv [file] - (map (comp (alter-field :timestamp (parse-date "M/d/y")) - (alter-field :transaction_type headify) - (alter-field :asset headify) - (alter-field :quantity_transacted bigdec) - (alter-field :usd_spot_price_at_transaction bigdec) - (alter-field :usd_amount_transacted bigdec)) + (map (comp (op/alter-field :timestamp (parse-date "M/d/y")) + (op/alter-field :transaction_type headify) + (op/alter-field :asset headify) + (op/alter-field :quantity_transacted bigdec) + (op/alter-field :usd_spot_price_at_transaction bigdec) + (op/alter-field :usd_amount_transacted bigdec)) (load-csv file))) (defn load-gdax-csv [file] - (map (comp (alter-field :type headify) - (alter-field :date (parse-date "y-M-d H:m:s")) - (split-field :amount [:txn_amount bigdec :txn_currency headify]) - (split-field :balance [:balance_amount bigdec :balance_currency headify])) + (map (comp (op/alter-field :type headify) + (op/alter-field :date (parse-date "y-M-d H:m:s")) + (op/split-field :amount [:txn_amount bigdec :txn_currency headify]) + (op/split-field :balance [:balance_amount bigdec :balance_currency headify])) (load-csv file))) (defn sha1-sum [s] @@ -99,7 +85,7 @@ ;; :notes) (defn common->local [txn] - (let [id-fields [::tax/timestamp + (let [id-fields [::tax/datetime ::tax/txn-type ::tax/amount ::tax/currency @@ -110,7 +96,7 @@ (defn common-coinbase->local [txn] (-> txn - (assoc ::tax/timestamp (:timestamp txn) + (assoc ::tax/datetime (:timestamp txn) ::tax/usd-amount (:usd_amount_transacted txn) ::tax/account :coinbase ::tax/amount (:quantity_transacted txn) @@ -131,7 +117,7 @@ (defmethod coinbase->local :receive [txn] (-> txn - (assoc ::tax/txn-type :receive) + (assoc ::tax/txn-type :deposit) (common-coinbase->local))) (defmethod coinbase->local :send [txn] @@ -150,9 +136,9 @@ (defn common-gdax->local [txn] (-> txn (assoc ::tax/txn-id (:txid txn) - ::tax/timestamp (:date txn) - ::tax/account :gdx - ::tax/amount (:txn_amount txn) + ::tax/datetime (:date txn) + ::tax/account :gdax + ::tax/amount (.abs (:txn_amount txn)) ::tax/currency (:txn_currency txn)) (common->local))) @@ -209,46 +195,70 @@ (defn load-bittrex-rates [file] (let [take-first-3 (fn [sym] (-> sym (subs 0 3) str/lower-case keyword)) take-rest (fn [sym] (-> sym (subs 3) str/lower-case keyword))] - (group-by (fn [measure] (.getTime (:date measure))) - (map (comp (alter-field :date (parse-date "y-M-d K-a")) - (add-field :from-currency (fn [row] (take-first-3 (:symbol row)))) - (add-field :to-currency (fn [row] (take-rest (:symbol row)))) - (alter-field :open bigdec) - (alter-field :high bigdec) - (alter-field :low bigdec) - (alter-field :close bigdec) - (alter-field :volume_from bigdec) - (alter-field :volume_to bigdec)) + (into {} + (map (fn [measure] {(.getTime (:date measure)) measure})) + (map (comp (op/alter-field :date (parse-date "y-M-d K-a")) + (op/add-field :from-currency (fn [row] (take-first-3 (:symbol row)))) + (op/add-field :to-currency (fn [row] (take-rest (:symbol row)))) + (op/alter-field :open bigdec) + (op/alter-field :high bigdec) + (op/alter-field :low bigdec) + (op/alter-field :close bigdec) + (op/alter-field :volume_from bigdec) + (op/alter-field :volume_to bigdec)) + (load-csv file))) + #_(group-by (fn [measure] (.getTime (:date measure))) + (map (comp (op/alter-field :date (parse-date "y-M-d K-a")) + (op/add-field :from-currency (fn [row] (take-first-3 (:symbol row)))) + (op/add-field :to-currency (fn [row] (take-rest (:symbol row)))) + (op/alter-field :open bigdec) + (op/alter-field :high bigdec) + (op/alter-field :low bigdec) + (op/alter-field :close bigdec) + (op/alter-field :volume_from bigdec) + (op/alter-field :volume_to bigdec)) (load-csv file))))) (s/fdef bittrex-load-prices - :args (s/cat :file file?) + :args (s/cat :file path-url?) :ret ::tax/pricemap) (defn load-coinmarketcap-rates [file from to] - (group-by (fn [measure] (.getTime (:date measure))) - (map (comp (alter-field :date (parse-date "M-d-y")) - (alter-field :open bigdec) - (alter-field :high bigdec) - (alter-field :low bigdec) - (alter-field :close bigdec) - (alter-field :volume bigdec) - (alter-field :market_cap bigdec) - (add-field :from-currency (fn [row] from)) - (add-field :to-currency (fn [row] to))) + (into {} + (map (fn [measure] {(.getTime (:date measure)) measure})) + (map (comp (op/alter-field :date (parse-date "M-d-y")) + (op/alter-field :open bigdec) + (op/alter-field :high bigdec) + (op/alter-field :low bigdec) + (op/alter-field :close bigdec) + (op/alter-field :volume bigdec) + (op/alter-field :market_cap bigdec) + (op/add-field :from-currency (fn [row] from)) + (op/add-field :to-currency (fn [row] to))) + (load-csv file))) + #_(group-by (fn [measure] (.getTime (:date measure))) + (map (comp (op/alter-field :date (parse-date "M-d-y")) + (op/alter-field :open bigdec) + (op/alter-field :high bigdec) + (op/alter-field :low bigdec) + (op/alter-field :close bigdec) + (op/alter-field :volume bigdec) + (op/alter-field :market_cap bigdec) + (op/add-field :from-currency (fn [row] from)) + (op/add-field :to-currency (fn [row] to))) (load-csv file)))) (s/fdef load-coinmarketcap-rates - :args (s/cat :file file? :from keyword? :to keyword?) + :args (s/cat :file path-url? :from keyword? :to keyword?) :ret ::tax/pricemap) (defn load-bittrex-csv [file] - (map (comp (split-field :exchange [:to-currency headify :from-currency headify] #"-") - (alter-field :type (fn [type-str] (headify (last (str/split type-str #"_"))))) - (alter-field :quantity bigdec) - (alter-field :limit bigdec) - (alter-field :commissionpaid bigdec) - (alter-field :price bigdec) - (alter-field :opened (parse-date "M/d/y K:m:s a")) - (alter-field :closed (parse-date "M/d/y K:m:s a"))) + (map (comp (op/split-field :exchange [:to-currency headify :from-currency headify] #"-") + (op/alter-field :type (fn [type-str] (headify (last (str/split type-str #"_"))))) + (op/alter-field :quantity bigdec) + (op/alter-field :limit bigdec) + (op/alter-field :commissionpaid bigdec) + (op/alter-field :price bigdec) + (op/alter-field :opened (parse-date "M/d/y K:m:s a")) + (op/alter-field :closed (parse-date "M/d/y K:m:s a"))) (load-csv file))) #_(defn minimize @@ -281,20 +291,22 @@ (defn bittrex-rate:btc->usd [btcusd] (let [timestamps (sort (keys btcusd))] (fn [date] - (first (map get-avg-rate - (get btcusd (bsearch-timestamps timestamps (.getTime date)))))))) + (get-avg-rate (get btcusd + (bsearch-timestamps timestamps + (.getTime date))))))) (s/fdef bittrex-rate:btc->usd :args (s/cat :btcusd ::tax/pricemap) - :ret (s/fspec :args (s/cat :date ::tax/timestamp) :ret decimal?)) + :ret (s/fspec :args (s/cat :date ::tax/datetime) :ret decimal?)) (defn bittrex-rate:bcc->usd [bccusd] (let [timestamps (sort (keys bccusd))] (fn [date] - (first (map get-avg-rate - (get bccusd (bsearch-timestamps timestamps (.getTime date)))))))) -(s/fdef bittrex-rate:btc->usd + (get-avg-rate (get bccusd + (bsearch-timestamps timestamps + (.getTime date))))))) +(s/fdef bittrex-rate:bcc->usd :args (s/cat :bccusd ::tax/pricemap) - :ret (s/fspec :args (s/cat :date ::tax/timestamp) :ret decimal?)) + :ret (s/fspec :args (s/cat :date ::tax/datetime) :ret decimal?)) (defn pp [obj] (clojure.pprint/pprint obj) @@ -304,52 +316,40 @@ (let [->usd (bittrex-rate:btc->usd btcusd) timestamps (sort (keys adabtc))] (fn [date] - (let [ada-in-btc (first (map get-avg-rate - (get adabtc (bsearch-timestamps timestamps - (.getTime date)))))] + (let [ada-in-btc (get-avg-rate (get adabtc + (bsearch-timestamps timestamps + (.getTime date))))] (* ada-in-btc (->usd date)))))) (s/fdef bittrex-rate:ada->usd :args (s/cat :btcusd ::tax/pricemap :adabtc ::tax/pricemap) - :ret (s/fspec :args (s/cat :date ::tax/timestamp) :ret decimal?)) + :ret (s/fspec :args (s/cat :date ::tax/datetime) :ret decimal?)) (defn bittrex-rate:eth->usd [ethusd] (let [timestamps (sort (keys ethusd))] (fn [date] - (first (map get-avg-rate - (get ethusd (bsearch-timestamps timestamps (.getTime date)))))))) + (get-avg-rate (get ethusd + (bsearch-timestamps timestamps + (.getTime date))))))) (s/fdef bittrex-rate:eth->usd :args (s/cat :ethusd ::tax/pricemap) - :ret (s/fspec :args (s/cat :date ::tax/timestamp) :ret decimal?)) + :ret (s/fspec :args (s/cat :date ::tax/datetime) :ret decimal?)) (defn coinmarketcap-rate:btg->usd [btgusd] (let [timestamps (sort (keys btgusd))] (fn [date] - (first (map get-avg-rate - (get btgusd (bsearch-timestamps timestamps (.getTime date)))))))) + (get-avg-rate (get btgusd + (bsearch-timestamps timestamps + (.getTime date))))))) (defn bittrex-rate:usdt->usd [] (fn [date] 1)) (s/fdef bittrex-rate:usdt->usd - :ret (s/fspec :args (s/cat :date ::tax/timestamp) :ret decimal?)) - -#_(defn build-bittrex-rates [btcusd-csv bccusd-csv adabtc-csv ethusd-csv] - (let [btcusd (load-bittrex-rates btcusd-csv)] - {:btc->usd (-> btcusd bittrex-rate:btc->usd) - :bcc->usd (-> bccusd-csv load-bittrex-rates bittrex-rate:bcc->usd) - :ada->usd (-> adabtc-csv load-bittrex-rates (partial bittrex-rate:ada->usd btcusd)) - :eth->usd (-> ethusd-csv load-bittrex-rates bittrex-rate:eth->usd) - :usdt->usd (bittrex-rate:usdt->usd)})) -#_(s/fdef build-bittrex-rates - :args (s/cat :btcusd-csv file? - :bccusd-csv file? - :adabtc-csv file? - :ethusd-csv file?) - :ret ::tax/pricemaps) + :ret (s/fspec :args (s/cat :date ::tax/datetime) :ret decimal?)) (defn common-bittrex->local [txn] (-> txn (assoc ::tax/txn-id (:orderuuid txn) - ::tax/timestamp (:closed txn) + ::tax/datetime (:closed txn) ::tax/account :bittrex) (common->local))) @@ -372,27 +372,29 @@ (common-bittrex->local)) (-> txn (assoc ::tax/txn-type :buy - ::tax/amount (with-precision 10 (/ (:quantity txn) - (:price txn))) + ::tax/amount (:price txn) + ;; ::tax/amount (with-precision 10 (/ (:quantity txn) + ;; (:price txn))) ::tax/currency (:to-currency txn) ::tax/usd-amount usd-amount ::tax/notes "split from sell") (common-bittrex->local))])) (defn bittrex-buy->local-txns [rates txn] - (let [usd-amount (* (:quantity txn) - (get-bittrex-rate rates (:from-currency txn) :usd (:closed txn)))] + (let [usd-amount (* (:price txn) + (get-bittrex-rate rates (:to-currency txn) :usd (:closed txn)))] [(-> txn - (assoc ::tax/txn-type :buy - ::tax/amount (:quantity txn) + (assoc ::tax/txn-type :sell + ::tax/amount (:price txn) ::tax/currency (:to-currency txn) ::tax/usd-amount usd-amount ::tax/notes "split from buy") (common-bittrex->local)) (-> txn - (assoc ::tax/txn-type :sell - ::tax/amount (with-precision 10 (/ (:quantity txn) - (:price txn))) + (assoc ::tax/txn-type :buy + ::tax/amount (:quantity txn) + ;; ::tax/amount (with-precision 10 (/ (:quantity txn) + ;; (:price txn))) ::tax/currency (:from-currency txn) ::tax/usd-amount usd-amount ::tax/notes "split from buy") @@ -409,5 +411,5 @@ (mapcat (partial bittrex-txn->local-txns rates) txns)) (s/fdef merge-bittrex-transactions :args (s/cat :rates ::tax/pricemaps - :txns (s/coll-of (s/map-of ::tax/header any?))) + :txns (s/coll-of (s/map-of ::header any?))) :ret ::tax/txns) diff --git a/src/taxer/ops.clj b/src/taxer/ops.clj index 0b5991c..c504e69 100644 --- a/src/taxer/ops.clj +++ b/src/taxer/ops.clj @@ -1,10 +1,15 @@ (ns taxer.ops (:require [taxer.core :as tax] - [clojure.spec.alpha :as s])) + [clojure.spec.alpha :as s] + [clojure.core.reducers :refer [reduce]] + [clojure.string :as str])) -(defn filter-on-value [rows field value] - (filter (fn [row] (= (get row field) value)) rows)) -(s/fdef filter-on-value +(defn filter-on-value [rows & filters] + (let [pairs (partition 2 filters)] + (filter (fn [row] + (every? (fn [[k v]] (= v (get row k))) pairs)) + rows))) +#_(s/fdef filter-on-value :args (s/cat :rows ::tax/txns :field keyword? :value any?) @@ -24,3 +29,24 @@ :without-order (s/cat :rows ::tax/txns :field keyword?)) :ret ::tax/txns) + + + +(defn alter-field [field f] + (fn [row] + (if (get row field) + (update row field (fn [value] (f value))) + row))) + +(defn add-field [field f] + (fn [row] + (assoc row field (f row)))) + +(defn split-field [field header-generators & [passed-sep]] + (let [header-pairs (partition 2 header-generators) + sep (or passed-sep #" ")] + (fn [row] + (into row + (map (fn [[header generator] value] {header (generator value)}) + header-pairs + (str/split (field row) sep))))))