coinsync 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +37 -6
- data/lib/coinsync/build_task.rb +3 -3
- data/lib/coinsync/config.rb +60 -4
- data/lib/coinsync/{currency_converter.rb → currency_conversion_task.rb} +19 -22
- data/lib/coinsync/currency_converters/base.rb +2 -1
- data/lib/coinsync/currency_converters/cache.rb +1 -1
- data/lib/coinsync/currency_converters/{fixer.rb → exchangeratesapi.rb} +3 -3
- data/lib/coinsync/importers/bitbay_api.rb +13 -7
- data/lib/coinsync/outputs/base.rb +2 -2
- data/lib/coinsync/outputs/list.rb +5 -5
- data/lib/coinsync/outputs/split_list.rb +153 -0
- data/lib/coinsync/outputs/summary.rb +2 -0
- data/lib/coinsync/price_loaders/all.rb +1 -0
- data/lib/coinsync/price_loaders/base.rb +61 -0
- data/lib/coinsync/price_loaders/cache.rb +34 -0
- data/lib/coinsync/price_loaders/cryptowatch.rb +34 -0
- data/lib/coinsync/transaction.rb +1 -1
- data/lib/coinsync/utils.rb +13 -0
- data/lib/coinsync/version.rb +1 -1
- data/lib/coinsync.rb +2 -1
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ee9560e9255a67e3389a7aa39ca48820dffe803
|
4
|
+
data.tar.gz: e946c4c12142d887c53f50b54c85d4a1d473f5bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cff97b38cecbc4933545d1f50525ab9aeba5e02ac13474e103e3108174b175bd98b6536097bec5816f4c265504fb155bc8d36081cdfdd6ef3f0987acbd5fd3f3
|
7
|
+
data.tar.gz: c324c154236885613861372a8302582dd80214416eaa3e85690826a0864ee43c610f95869c78c0e174e8ae9d882f176657bb476909a14d5cef808471abfad6d5
|
data/README.md
CHANGED
@@ -88,7 +88,8 @@ settings:
|
|
88
88
|
time_format: "%Y-%m-%d %H:%M"
|
89
89
|
column_separator: ";"
|
90
90
|
decimal_separator: ","
|
91
|
-
|
91
|
+
convert_currency:
|
92
|
+
to: PLN
|
92
93
|
include:
|
93
94
|
- extras.rb
|
94
95
|
```
|
@@ -119,8 +120,7 @@ See the separate ["Importers"](doc/importers.md) doc file for a full list of sup
|
|
119
120
|
|
120
121
|
- `base_cryptocurrencies`: an array listing which cryptocurrencies might be considered the base currency for a trading pair; if both sides of the pair are included in the list, the one earlier in the list takes priority (default: `['USDT', 'BTC', 'ETH', 'BNB', 'KCS', 'LTC', 'BCH', 'NEO']`)
|
121
122
|
- `column_separator`: what character is used to separate columns in saved CSV files (default: `","`)
|
122
|
-
- `
|
123
|
-
- `convert_with`: what currency converter module should be used to do the currency conversions (default: `fixer`)
|
123
|
+
- `convert_currency`: currency conversion config, see below
|
124
124
|
- `decimal_separator`: what character is used to separate decimal digits in numbers (default: `"."`)
|
125
125
|
- `time_format`: the [time format string](http://ruby-doc.org/core-2.5.0/Time.html#method-i-strftime) to use when printing dates (default: `"%Y-%m-%d %H:%M:%S"`)
|
126
126
|
- `timezone`: an explicit timezone to use for printing dates and currency conversion (default: system timezone)
|
@@ -132,13 +132,35 @@ If you want to extend the tool with support for additional importers, build task
|
|
132
132
|
|
133
133
|
### Currency conversion
|
134
134
|
|
135
|
-
If you make transactions in multiple fiat currencies (e.g. USD on Bitfinex, EUR on Kraken) and you want to have all values converted to one currency (for example, to calculate profits for tax purposes),
|
135
|
+
If you make transactions in multiple fiat currencies (e.g. USD on Bitfinex, EUR on Kraken) and you want to have all values converted to one currency (for example, to calculate profits for tax purposes), add a `currency_conversion` section in the settings. Currency conversion is done using pluggable modules that load currency rates from specific sources. Currently, two are available:
|
136
136
|
|
137
|
-
- `
|
138
|
-
- `nbp` loads rates from [Polish National Bank](http://www.nbp.pl/home.aspx?f=/statystyka/kursy.html) (this
|
137
|
+
- `exchangeratesapi` loads exchange rates from [exchangeratesapi.io](https://exchangeratesapi.io) API
|
138
|
+
- `nbp` loads rates from [Polish National Bank](http://www.nbp.pl/home.aspx?f=/statystyka/kursy.html) (this might be moved to a separate gem?)
|
139
139
|
|
140
140
|
You can always write another module that connects to your preferred source and plug it in using `include`.
|
141
141
|
|
142
|
+
The `currency_conversion` option value should be a hash with keys:
|
143
|
+
|
144
|
+
- `using`: name of the currency converter module (default: `exchangeratesapi`)
|
145
|
+
- `to`: code of the currency to convert to (required)
|
146
|
+
|
147
|
+
|
148
|
+
### Transaction value estimation
|
149
|
+
|
150
|
+
In some cases you might want to know the total value of a transaction in a chosen fiat currency. For purchase and sale transactions, this is just the total amount for which you've bought or sold the given asset. However, for swap (crypto-to-crypto) transactions, the total value can't be simply calculated from the available data, and it might not even be obvious *how* it should be calculated at all.
|
151
|
+
|
152
|
+
This is where value estimation modules aka price loaders come in. They're another type of pluggable modules that load historical prices of a given coin from a selected source. For simplicity, only the price of the base coin is checked - e.g. when you buy STEEM with BTC, the value of the transaction (i.e. the value of both the sold BTC and the bought STEEM) is set to the price of BTC at that moment times the amount of BTC spent, and the price of STEEM in USD/EUR isn't checked separately.
|
153
|
+
|
154
|
+
Currently only one price loader is available: `cryptowatch`, which can load the price of any coin listed on [Cryptowat.ch](https://cryptowat.ch) (requires the [cointools gem](https://github.com/mackuba/cointools)).
|
155
|
+
|
156
|
+
To estimate transaction value using Cryptowat.ch, add a `value_estimation` section in the settings:
|
157
|
+
|
158
|
+
- `using`: name of the price loader module (required - `cryptowatch`)
|
159
|
+
- `exchange`: name of an exchange listed on Cryptowat.ch (default: `bitfinex`)
|
160
|
+
- `currency`: code of the fiat currency in which value should be calculated (default: `USD`)
|
161
|
+
|
162
|
+
At the moment this feature is only used in the [Split List](#build-split-list) output.
|
163
|
+
|
142
164
|
|
143
165
|
## Using the tool
|
144
166
|
|
@@ -195,6 +217,15 @@ coinsync build list
|
|
195
217
|
This will just print all your transactions to a single unified CSV file (in `build/list.csv`).
|
196
218
|
|
197
219
|
|
220
|
+
#### Build Split List
|
221
|
+
|
222
|
+
```
|
223
|
+
coinsync build split-list
|
224
|
+
```
|
225
|
+
|
226
|
+
Builds a list similar to `build list`, but all "swap" transactions (crypto-to-crypto) are split into separate sale and purchase parts (`build/split-list.csv`). This can be useful for some tax-related calculations, and is only really useful if you also enable the [transaction value estimation option](#transaction-value-estimation).
|
227
|
+
|
228
|
+
|
198
229
|
#### Build Raw
|
199
230
|
|
200
231
|
```
|
data/lib/coinsync/build_task.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'fileutils'
|
2
2
|
|
3
3
|
require_relative 'builder'
|
4
|
-
require_relative '
|
4
|
+
require_relative 'currency_conversion_task'
|
5
5
|
require_relative 'outputs/all'
|
6
6
|
|
7
7
|
module CoinSync
|
@@ -31,8 +31,8 @@ module CoinSync
|
|
31
31
|
output = output_class.new(@config, "build/#{output_name}.csv")
|
32
32
|
|
33
33
|
if output.requires_currency_conversion?
|
34
|
-
if @config.
|
35
|
-
converter =
|
34
|
+
if options = @config.currency_conversion
|
35
|
+
converter = CurrencyConversionTask.new(options)
|
36
36
|
converter.process_transactions(transactions)
|
37
37
|
end
|
38
38
|
end
|
data/lib/coinsync/config.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
|
+
require 'ostruct'
|
1
2
|
require 'yaml'
|
2
3
|
|
4
|
+
require_relative 'currencies'
|
5
|
+
require_relative 'currency_converters/all'
|
6
|
+
require_relative 'price_loaders/all'
|
3
7
|
require_relative 'source'
|
4
8
|
|
5
9
|
module CoinSync
|
@@ -71,12 +75,12 @@ module CoinSync
|
|
71
75
|
settings['decimal_separator']
|
72
76
|
end
|
73
77
|
|
74
|
-
def
|
75
|
-
settings['
|
78
|
+
def currency_conversion
|
79
|
+
settings['convert_currency'] && CurrencyConversionOptions.new(settings['convert_currency'])
|
76
80
|
end
|
77
81
|
|
78
|
-
def
|
79
|
-
settings['
|
82
|
+
def value_estimation
|
83
|
+
settings['estimate_value'] && ValueEstimationOptions.new(settings['estimate_value'])
|
80
84
|
end
|
81
85
|
|
82
86
|
def time_format
|
@@ -90,5 +94,57 @@ module CoinSync
|
|
90
94
|
def translate(label)
|
91
95
|
@labels[label] || label
|
92
96
|
end
|
97
|
+
|
98
|
+
class CurrencyConversionOptions < OpenStruct
|
99
|
+
DEFAULT_CURRENCY_CONVERTER = :exchangeratesapi
|
100
|
+
|
101
|
+
def initialize(options)
|
102
|
+
super
|
103
|
+
|
104
|
+
if options['using']
|
105
|
+
self.currency_converter_name = options['using'].to_sym
|
106
|
+
else
|
107
|
+
self.currency_converter_name = DEFAULT_CURRENCY_CONVERTER
|
108
|
+
end
|
109
|
+
|
110
|
+
if options['to']
|
111
|
+
self.currency = FiatCurrency.new(options['to'].upcase)
|
112
|
+
else
|
113
|
+
raise "'convert_currency' requires a 'to' field with a currency code"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def currency_converter
|
118
|
+
currency_converter_class = CurrencyConverters.registered[currency_converter_name]
|
119
|
+
|
120
|
+
if currency_converter_class
|
121
|
+
currency_converter_class.new(self)
|
122
|
+
else
|
123
|
+
raise "Unknown currency converter: #{currency_converter_name}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class ValueEstimationOptions < OpenStruct
|
129
|
+
def initialize(options)
|
130
|
+
super
|
131
|
+
|
132
|
+
if options['using']
|
133
|
+
self.price_loader_name = options['using'].to_sym
|
134
|
+
else
|
135
|
+
raise "'value_estimation' requires a 'using' field with a name of a price loader"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def price_loader
|
140
|
+
price_loader_class = PriceLoaders.registered[price_loader_name]
|
141
|
+
|
142
|
+
if price_loader_class
|
143
|
+
price_loader_class.new(self)
|
144
|
+
else
|
145
|
+
raise "Unknown price loader: #{price_loader_name}"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
93
149
|
end
|
94
150
|
end
|
@@ -1,23 +1,14 @@
|
|
1
1
|
require 'bigdecimal'
|
2
2
|
require 'time'
|
3
3
|
|
4
|
-
require_relative 'currencies'
|
5
|
-
require_relative 'currency_converters/all'
|
6
4
|
require_relative 'transaction'
|
7
5
|
|
8
6
|
module CoinSync
|
9
|
-
class
|
10
|
-
def initialize(
|
11
|
-
@
|
12
|
-
@target_currency =
|
13
|
-
|
14
|
-
converter_class = CurrencyConverters.registered[config.currency_converter]
|
15
|
-
|
16
|
-
if converter_class
|
17
|
-
@converter = converter_class.new
|
18
|
-
else
|
19
|
-
raise "Unknown currency converter #{config.currency_converter}"
|
20
|
-
end
|
7
|
+
class CurrencyConversionTask
|
8
|
+
def initialize(options)
|
9
|
+
@options = options
|
10
|
+
@target_currency = options.currency
|
11
|
+
@converter = options.currency_converter
|
21
12
|
end
|
22
13
|
|
23
14
|
def process_transactions(transactions)
|
@@ -26,16 +17,22 @@ module CoinSync
|
|
26
17
|
|
27
18
|
if tx.bought_currency.fiat? && tx.bought_currency != @target_currency
|
28
19
|
tx.converted = Transaction::ConvertedAmounts.new
|
29
|
-
tx.converted.bought_currency = @target_currency
|
30
|
-
tx.converted.exchange_rate = @converter.convert(
|
31
|
-
BigDecimal.new(1),
|
32
|
-
from: tx.bought_currency,
|
33
|
-
to: @target_currency,
|
34
|
-
date: tx.time.to_date
|
35
|
-
)
|
36
|
-
tx.converted.bought_amount = tx.bought_amount * tx.converted.exchange_rate
|
37
20
|
tx.converted.sold_currency = tx.sold_currency
|
38
21
|
tx.converted.sold_amount = tx.sold_amount
|
22
|
+
tx.converted.bought_currency = @target_currency
|
23
|
+
|
24
|
+
if tx.bought_currency.code
|
25
|
+
tx.converted.exchange_rate = @converter.convert(
|
26
|
+
BigDecimal.new(1),
|
27
|
+
from: tx.bought_currency,
|
28
|
+
to: @target_currency,
|
29
|
+
date: tx.time.to_date
|
30
|
+
)
|
31
|
+
tx.converted.bought_amount = tx.bought_amount * tx.converted.exchange_rate
|
32
|
+
else
|
33
|
+
tx.converted.exchange_rate = nil
|
34
|
+
tx.converted.bought_amount = BigDecimal.new(0)
|
35
|
+
end
|
39
36
|
elsif tx.sold_currency.fiat? && tx.sold_currency != @target_currency
|
40
37
|
tx.converted = Transaction::ConvertedAmounts.new
|
41
38
|
tx.converted.bought_currency = tx.bought_currency
|
@@ -6,10 +6,10 @@ require_relative '../request'
|
|
6
6
|
|
7
7
|
module CoinSync
|
8
8
|
module CurrencyConverters
|
9
|
-
class
|
10
|
-
register_converter :
|
9
|
+
class ExchangeRatesAPI < Base
|
10
|
+
register_converter :exchangeratesapi
|
11
11
|
|
12
|
-
BASE_URL = "https://
|
12
|
+
BASE_URL = "https://exchangeratesapi.io/api"
|
13
13
|
|
14
14
|
class Exception < StandardError; end
|
15
15
|
class NoDataException < Exception; end
|
@@ -22,7 +22,7 @@ module CoinSync
|
|
22
22
|
OP_SALE = '-pay_for_currency'
|
23
23
|
OP_FEE = '-fee'
|
24
24
|
|
25
|
-
MAX_TIME_DIFFERENCE =
|
25
|
+
MAX_TIME_DIFFERENCE = 2.0 # TODO: this breaks too easily (3.0)
|
26
26
|
TRANSACTION_TYPES = [OP_PURCHASE, OP_SALE, OP_FEE]
|
27
27
|
|
28
28
|
class HistoryEntry
|
@@ -45,17 +45,23 @@ module CoinSync
|
|
45
45
|
|
46
46
|
def parse_currency(code)
|
47
47
|
case code.upcase
|
48
|
-
|
49
|
-
when 'ETH' then CryptoCurrency.new('ETH')
|
50
|
-
when 'LTC' then CryptoCurrency.new('LTC')
|
51
|
-
when 'LSK' then CryptoCurrency.new('LSK')
|
48
|
+
|
52
49
|
when 'BCC' then CryptoCurrency.new('BCH')
|
50
|
+
when 'BTC' then CryptoCurrency.new('BTC')
|
53
51
|
when 'BTG' then CryptoCurrency.new('BTG')
|
54
|
-
when 'GAME' then CryptoCurrency.new('GAME')
|
55
52
|
when 'DASH' then CryptoCurrency.new('DASH')
|
56
|
-
when '
|
53
|
+
when 'ETH' then CryptoCurrency.new('ETH')
|
54
|
+
when 'GAME' then CryptoCurrency.new('GAME')
|
55
|
+
when 'KZC' then CryptoCurrency.new('KZC')
|
56
|
+
when 'LSK' then CryptoCurrency.new('LSK')
|
57
|
+
when 'LTC' then CryptoCurrency.new('LTC')
|
58
|
+
when 'XIN' then CryptoCurrency.new('XIN')
|
59
|
+
when 'XRP' then CryptoCurrency.new('XRP')
|
60
|
+
|
57
61
|
when 'EUR' then FiatCurrency.new('EUR')
|
58
62
|
when 'USD' then FiatCurrency.new('USD')
|
63
|
+
when 'PLN' then FiatCurrency.new('PLN')
|
64
|
+
|
59
65
|
else raise "Unknown currency: #{code}"
|
60
66
|
end
|
61
67
|
end
|
@@ -9,10 +9,10 @@ module CoinSync
|
|
9
9
|
|
10
10
|
class Base
|
11
11
|
def self.register_output(key)
|
12
|
-
if Outputs.registered[key]
|
12
|
+
if Outputs.registered[key.to_sym]
|
13
13
|
raise "Output has already been registered at '#{key}'"
|
14
14
|
else
|
15
|
-
Outputs.registered[key] = self
|
15
|
+
Outputs.registered[key.to_sym] = self
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
@@ -34,10 +34,10 @@ module CoinSync
|
|
34
34
|
'Currency'
|
35
35
|
].map { |l| @config.translate(l) }
|
36
36
|
|
37
|
-
if
|
37
|
+
if options = @config.currency_conversion
|
38
38
|
line += [
|
39
|
-
@config.translate('Total value ($CURRENCY)').gsub('$CURRENCY', currency.code),
|
40
|
-
@config.translate('Price ($CURRENCY)').gsub('$CURRENCY', currency.code),
|
39
|
+
@config.translate('Total value ($CURRENCY)').gsub('$CURRENCY', options.currency.code),
|
40
|
+
@config.translate('Price ($CURRENCY)').gsub('$CURRENCY', options.currency.code),
|
41
41
|
@config.translate('Exchange rate')
|
42
42
|
]
|
43
43
|
end
|
@@ -66,7 +66,7 @@ module CoinSync
|
|
66
66
|
tx.fiat_currency.code || '–'
|
67
67
|
]
|
68
68
|
|
69
|
-
if @config.
|
69
|
+
if @config.currency_conversion
|
70
70
|
if tx.converted
|
71
71
|
csv += [
|
72
72
|
@formatter.format_fiat(tx.converted.fiat_amount),
|
@@ -112,7 +112,7 @@ module CoinSync
|
|
112
112
|
currency.code
|
113
113
|
]
|
114
114
|
|
115
|
-
if @config.
|
115
|
+
if @config.currency_conversion
|
116
116
|
csv += [nil, nil, nil]
|
117
117
|
end
|
118
118
|
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative '../currencies'
|
5
|
+
require_relative '../currency_conversion_task'
|
6
|
+
require_relative '../transaction'
|
7
|
+
|
8
|
+
module CoinSync
|
9
|
+
module Outputs
|
10
|
+
class SplitList < List
|
11
|
+
register_output 'split-list'
|
12
|
+
|
13
|
+
def requires_currency_conversion?
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(config, target_file)
|
18
|
+
super
|
19
|
+
|
20
|
+
if @config.value_estimation
|
21
|
+
@price_loader = @config.value_estimation.price_loader
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def process_transactions(transactions, *args)
|
26
|
+
split_list = []
|
27
|
+
|
28
|
+
transactions.each do |tx|
|
29
|
+
if tx.purchase? || tx.sale?
|
30
|
+
split_list << tx
|
31
|
+
else
|
32
|
+
sale, purchase = split_transaction(tx)
|
33
|
+
split_list << sale
|
34
|
+
split_list << purchase
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
@price_loader&.finalize
|
39
|
+
|
40
|
+
if options = @config.currency_conversion
|
41
|
+
converter = CurrencyConversionTask.new(options)
|
42
|
+
converter.process_transactions(split_list)
|
43
|
+
end
|
44
|
+
|
45
|
+
super(split_list, *args)
|
46
|
+
end
|
47
|
+
|
48
|
+
def split_transaction(tx)
|
49
|
+
if @classifier.is_purchase?(tx)
|
50
|
+
base = tx.sold_currency
|
51
|
+
base_price, fiat_currency = get_coin_price(base, tx.time)
|
52
|
+
total_value = tx.sold_amount * base_price
|
53
|
+
else
|
54
|
+
base = tx.bought_currency
|
55
|
+
base_price, fiat_currency = get_coin_price(base, tx.time)
|
56
|
+
total_value = tx.bought_amount * base_price
|
57
|
+
end
|
58
|
+
|
59
|
+
sale = Transaction.new(
|
60
|
+
number: "#{tx.number}.A",
|
61
|
+
exchange: tx.exchange,
|
62
|
+
time: tx.time,
|
63
|
+
sold_currency: tx.sold_currency,
|
64
|
+
sold_amount: tx.sold_amount,
|
65
|
+
bought_currency: fiat_currency,
|
66
|
+
bought_amount: total_value
|
67
|
+
)
|
68
|
+
|
69
|
+
purchase = Transaction.new(
|
70
|
+
number: "#{tx.number}.B",
|
71
|
+
exchange: tx.exchange,
|
72
|
+
time: tx.time,
|
73
|
+
bought_currency: tx.bought_currency,
|
74
|
+
bought_amount: tx.bought_amount,
|
75
|
+
sold_currency: fiat_currency,
|
76
|
+
sold_amount: total_value
|
77
|
+
)
|
78
|
+
|
79
|
+
[sale, purchase]
|
80
|
+
end
|
81
|
+
|
82
|
+
def fiat_transaction_to_csv(tx)
|
83
|
+
tx_type = @config.translate(tx.type.to_s.capitalize)
|
84
|
+
|
85
|
+
is_split = tx.number.to_s.include?('.')
|
86
|
+
is_incomplete = is_split && tx.bought_amount * tx.sold_amount == 0
|
87
|
+
|
88
|
+
if is_split
|
89
|
+
tx_type = @config.translate(Transaction::TYPE_SWAP.to_s.capitalize) + '/' + tx_type
|
90
|
+
end
|
91
|
+
|
92
|
+
csv = [
|
93
|
+
tx.number || 0,
|
94
|
+
tx.exchange,
|
95
|
+
tx_type,
|
96
|
+
@formatter.format_time(tx.time),
|
97
|
+
@formatter.format_crypto(tx.crypto_amount),
|
98
|
+
tx.crypto_currency.code
|
99
|
+
]
|
100
|
+
|
101
|
+
if is_incomplete
|
102
|
+
csv += [nil, nil, nil]
|
103
|
+
else
|
104
|
+
csv += [
|
105
|
+
@formatter.format_fiat(tx.fiat_amount),
|
106
|
+
@formatter.format_fiat_price(tx.price),
|
107
|
+
tx.fiat_currency.code || '–'
|
108
|
+
]
|
109
|
+
end
|
110
|
+
|
111
|
+
if @config.currency_conversion
|
112
|
+
if is_incomplete
|
113
|
+
csv += [nil, nil, nil]
|
114
|
+
elsif tx.converted
|
115
|
+
csv += [
|
116
|
+
@formatter.format_fiat(tx.converted.fiat_amount),
|
117
|
+
@formatter.format_fiat_price(tx.converted.price),
|
118
|
+
tx.converted.exchange_rate && @formatter.format_float(tx.converted.exchange_rate, precision: 4)
|
119
|
+
]
|
120
|
+
else
|
121
|
+
csv += [
|
122
|
+
@formatter.format_fiat(tx.fiat_amount),
|
123
|
+
@formatter.format_fiat_price(tx.price),
|
124
|
+
nil
|
125
|
+
]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
csv
|
130
|
+
end
|
131
|
+
|
132
|
+
def swap_transaction_to_csv(tx)
|
133
|
+
# sanity check - this should not happen
|
134
|
+
raise "SplitList: unexpected unprocessed swap transaction"
|
135
|
+
end
|
136
|
+
|
137
|
+
def get_coin_price(coin, time)
|
138
|
+
if @price_loader
|
139
|
+
print "$"
|
140
|
+
|
141
|
+
begin
|
142
|
+
@price_loader.get_price(coin, time)
|
143
|
+
rescue Exception => e
|
144
|
+
@price_loader.finalize
|
145
|
+
raise
|
146
|
+
end
|
147
|
+
else
|
148
|
+
[BigDecimal.new(0), FiatCurrency.new(nil)]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -17,6 +17,8 @@ module CoinSync
|
|
17
17
|
totals = Hash.new { BigDecimal(0) }
|
18
18
|
|
19
19
|
transactions.each do |tx|
|
20
|
+
break if args.first.to_i > 0 && tx.time.year >= args.first.to_i
|
21
|
+
|
20
22
|
if tx.bought_currency.crypto?
|
21
23
|
amount = totals[tx.bought_currency]
|
22
24
|
totals[tx.bought_currency] = amount + tx.bought_amount
|
@@ -0,0 +1 @@
|
|
1
|
+
Dir[File.join(File.dirname(__FILE__), '*.rb')].each { |f| require(f) }
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
require_relative 'cache'
|
4
|
+
require_relative '../currencies'
|
5
|
+
|
6
|
+
module CoinSync
|
7
|
+
module PriceLoaders
|
8
|
+
def self.registered
|
9
|
+
@price_loaders ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
class Base
|
13
|
+
def self.register_price_loader(key)
|
14
|
+
if PriceLoaders.registered[key.to_sym]
|
15
|
+
raise "Price loader has already been registered at '#{key}'"
|
16
|
+
else
|
17
|
+
PriceLoaders.registered[key.to_sym] = self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(options)
|
22
|
+
@options = options
|
23
|
+
@currency = currency
|
24
|
+
@cache = Cache.new(cache_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def cache_name
|
28
|
+
self.class.name.downcase.split('::').last
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_price(coin, time)
|
32
|
+
(coin.is_a?(CryptoCurrency)) or raise "#{self.class}: 'coin' should be a CryptoCurrency"
|
33
|
+
(time.is_a?(Time)) or raise "#{self.class}: 'time' should be a Time"
|
34
|
+
|
35
|
+
data = @cache[coin, time]
|
36
|
+
|
37
|
+
if data.nil?
|
38
|
+
data = fetch_price(coin, time)
|
39
|
+
@cache[coin, time] = data
|
40
|
+
end
|
41
|
+
|
42
|
+
price = data.is_a?(Array) ? data.first : data
|
43
|
+
|
44
|
+
[convert_price(price), @currency]
|
45
|
+
end
|
46
|
+
|
47
|
+
def convert_price(price)
|
48
|
+
case price
|
49
|
+
when BigDecimal then price
|
50
|
+
when String, Integer then BigDecimal.new(price)
|
51
|
+
when Float then BigDecimal.new(price, 0)
|
52
|
+
else raise "Unexpected price value: #{price.inspect}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def finalize
|
57
|
+
@cache.save
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module CoinSync
|
5
|
+
module PriceLoaders
|
6
|
+
class Cache
|
7
|
+
def initialize(name)
|
8
|
+
@name = name
|
9
|
+
@filename = "data/prices/#{name}.json"
|
10
|
+
|
11
|
+
if File.exist?(@filename)
|
12
|
+
@prices = JSON.parse(File.read(@filename))
|
13
|
+
else
|
14
|
+
@prices = {}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](coin, time)
|
19
|
+
@prices[coin.code] ||= {}
|
20
|
+
@prices[coin.code][time.to_i.to_s]
|
21
|
+
end
|
22
|
+
|
23
|
+
def []=(coin, time, price)
|
24
|
+
@prices[coin.code] ||= {}
|
25
|
+
@prices[coin.code][time.to_i.to_s] = price
|
26
|
+
end
|
27
|
+
|
28
|
+
def save
|
29
|
+
FileUtils.mkdir_p(File.dirname(@filename))
|
30
|
+
File.write(@filename, JSON.generate(@prices))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require_relative '../utils'
|
3
|
+
|
4
|
+
module CoinSync
|
5
|
+
module PriceLoaders
|
6
|
+
class Cryptowatch < Base
|
7
|
+
register_price_loader :cryptowatch
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
options.currency = options.currency&.upcase || 'USD'
|
11
|
+
options.exchange ||= 'bitfinex'
|
12
|
+
|
13
|
+
super
|
14
|
+
|
15
|
+
Utils.lazy_require(self, 'cointools')
|
16
|
+
|
17
|
+
@cryptowatch ||= CoinTools::Cryptowatch.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def cache_name
|
21
|
+
"cryptowatch-#{@options.exchange}-#{@options.currency.downcase}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def currency
|
25
|
+
FiatCurrency.new(@options.currency)
|
26
|
+
end
|
27
|
+
|
28
|
+
def fetch_price(coin, time)
|
29
|
+
result = @cryptowatch.get_price_fast(@options.exchange, coin.code.downcase + @options.currency.downcase, time)
|
30
|
+
[result.price, result.time.to_i]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/coinsync/transaction.rb
CHANGED
@@ -105,7 +105,7 @@ module CoinSync
|
|
105
105
|
raise "Transaction: '#{bought_currency}' is not a valid currency"
|
106
106
|
end
|
107
107
|
|
108
|
-
(bought_amount
|
108
|
+
(bought_amount >= 0) or raise "Transaction: bought_amount should not be negative (#{bought_amount})"
|
109
109
|
|
110
110
|
if sold_amount.is_a?(BigDecimal)
|
111
111
|
@sold_amount = sold_amount
|
data/lib/coinsync/version.rb
CHANGED
data/lib/coinsync.rb
CHANGED
@@ -5,13 +5,14 @@ require "coinsync/builder"
|
|
5
5
|
require "coinsync/config"
|
6
6
|
require "coinsync/crypto_classifier"
|
7
7
|
require "coinsync/currencies"
|
8
|
-
require "coinsync/
|
8
|
+
require "coinsync/currency_conversion_task"
|
9
9
|
require "coinsync/formatter"
|
10
10
|
require "coinsync/import_task"
|
11
11
|
require "coinsync/request"
|
12
12
|
require "coinsync/run_command_task"
|
13
13
|
require "coinsync/source"
|
14
14
|
require "coinsync/transaction"
|
15
|
+
require "coinsync/utils"
|
15
16
|
require "coinsync/version"
|
16
17
|
|
17
18
|
module CoinSync
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: coinsync
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kuba Suder
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-04-
|
11
|
+
date: 2018-04-24 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -30,11 +30,11 @@ files:
|
|
30
30
|
- lib/coinsync/config.rb
|
31
31
|
- lib/coinsync/crypto_classifier.rb
|
32
32
|
- lib/coinsync/currencies.rb
|
33
|
-
- lib/coinsync/
|
33
|
+
- lib/coinsync/currency_conversion_task.rb
|
34
34
|
- lib/coinsync/currency_converters/all.rb
|
35
35
|
- lib/coinsync/currency_converters/base.rb
|
36
36
|
- lib/coinsync/currency_converters/cache.rb
|
37
|
-
- lib/coinsync/currency_converters/
|
37
|
+
- lib/coinsync/currency_converters/exchangeratesapi.rb
|
38
38
|
- lib/coinsync/currency_converters/nbp.rb
|
39
39
|
- lib/coinsync/formatter.rb
|
40
40
|
- lib/coinsync/import_task.rb
|
@@ -60,13 +60,19 @@ files:
|
|
60
60
|
- lib/coinsync/outputs/base.rb
|
61
61
|
- lib/coinsync/outputs/list.rb
|
62
62
|
- lib/coinsync/outputs/raw.rb
|
63
|
+
- lib/coinsync/outputs/split_list.rb
|
63
64
|
- lib/coinsync/outputs/summary.rb
|
65
|
+
- lib/coinsync/price_loaders/all.rb
|
66
|
+
- lib/coinsync/price_loaders/base.rb
|
67
|
+
- lib/coinsync/price_loaders/cache.rb
|
68
|
+
- lib/coinsync/price_loaders/cryptowatch.rb
|
64
69
|
- lib/coinsync/request.rb
|
65
70
|
- lib/coinsync/run_command_task.rb
|
66
71
|
- lib/coinsync/source.rb
|
67
72
|
- lib/coinsync/source_filter.rb
|
68
73
|
- lib/coinsync/table_printer.rb
|
69
74
|
- lib/coinsync/transaction.rb
|
75
|
+
- lib/coinsync/utils.rb
|
70
76
|
- lib/coinsync/version.rb
|
71
77
|
homepage: https://github.com/mackuba/coinsync
|
72
78
|
licenses:
|