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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 59e420867e256a9ed7fef1d87ae08279e7e4dfcc
4
- data.tar.gz: 69fbe095ab411e0a0a65223063bb30267af81682
3
+ metadata.gz: 5ee9560e9255a67e3389a7aa39ca48820dffe803
4
+ data.tar.gz: e946c4c12142d887c53f50b54c85d4a1d473f5bf
5
5
  SHA512:
6
- metadata.gz: fa574e1fcecefe950ab83f576754e24ae4a867bf913ff2ea34cdf0673149e6787edc75663d6500500dec5e8e668fb5a6e34163e0ee106395296d6beef721cc11
7
- data.tar.gz: 8f60e49910ffc061406cf20f7186f31bd0246e2c663b69753f343daf87c5b155b2a82d0039095c36ebf1ccea0bf559cba4b9c3b2eaacf8cbcbbd273206bc670b
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
- convert_to: PLN
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
- - `convert_to`: what fiat currency should fiat amounts be converted to (default: none)
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), use the `convert_to` and `convert_with` options in the settings. Currency conversion is done using pluggable modules that load currency rates from specific sources. Currently, two are available:
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
- - `fixer` loads exchange rates from [fixer.io](http://fixer.io) API (note: they've now decided to deprecate this API in June and the new one requires an API key, let me know if you know any better option)
138
- - `nbp` loads rates from [Polish National Bank](http://www.nbp.pl/home.aspx?f=/statystyka/kursy.html) (this will be moved to a separate gem)
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
  ```
@@ -1,7 +1,7 @@
1
1
  require 'fileutils'
2
2
 
3
3
  require_relative 'builder'
4
- require_relative 'currency_converter'
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.convert_to_currency
35
- converter = CurrencyConverter.new(@config)
34
+ if options = @config.currency_conversion
35
+ converter = CurrencyConversionTask.new(options)
36
36
  converter.process_transactions(transactions)
37
37
  end
38
38
  end
@@ -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 convert_to_currency
75
- settings['convert_to'] ? FiatCurrency.new(settings['convert_to']) : nil
78
+ def currency_conversion
79
+ settings['convert_currency'] && CurrencyConversionOptions.new(settings['convert_currency'])
76
80
  end
77
81
 
78
- def currency_converter
79
- settings['convert_with']&.to_sym || :fixer
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 CurrencyConverter
10
- def initialize(config)
11
- @config = config
12
- @target_currency = config.convert_to_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
@@ -18,7 +18,8 @@ module CoinSync
18
18
  end
19
19
  end
20
20
 
21
- def initialize
21
+ def initialize(options)
22
+ @options = options
22
23
  @cache = Cache.new(self.class.name.downcase.split('::').last)
23
24
  end
24
25
 
@@ -6,7 +6,7 @@ module CoinSync
6
6
  class Cache
7
7
  def initialize(name)
8
8
  @name = name
9
- @filename = "caches/#{name}.json"
9
+ @filename = "cache/#{name}.json"
10
10
 
11
11
  if File.exist?(@filename)
12
12
  @rates = JSON.parse(File.read(@filename))
@@ -6,10 +6,10 @@ require_relative '../request'
6
6
 
7
7
  module CoinSync
8
8
  module CurrencyConverters
9
- class Fixer < Base
10
- register_converter :fixer
9
+ class ExchangeRatesAPI < Base
10
+ register_converter :exchangeratesapi
11
11
 
12
- BASE_URL = "https://api.fixer.io"
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 = 5.0
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
- when 'BTC' then CryptoCurrency.new('BTC')
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 'PLN' then FiatCurrency.new('PLN')
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 currency = @config.convert_to_currency
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.convert_to_currency
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.convert_to_currency
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
@@ -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 > 0) or raise "Transaction: bought_amount should be positive (#{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
@@ -0,0 +1,13 @@
1
+ module CoinSync
2
+ module Utils
3
+ def self.lazy_require(source, name)
4
+ begin
5
+ require(name)
6
+ rescue LoadError
7
+ gem = name.split('/').first
8
+ puts "#{source.class}: gem '#{gem}' is not installed"
9
+ exit 1
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,3 @@
1
1
  module CoinSync
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
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/currency_converter"
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.1.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-09 00:00:00.000000000 Z
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/currency_converter.rb
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/fixer.rb
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: