coinsync 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE.txt +21 -0
  3. data/README.md +255 -0
  4. data/bin/coinsync +78 -0
  5. data/doc/importers.md +201 -0
  6. data/lib/coinsync.rb +18 -0
  7. data/lib/coinsync/balance.rb +19 -0
  8. data/lib/coinsync/balance_task.rb +65 -0
  9. data/lib/coinsync/build_task.rb +43 -0
  10. data/lib/coinsync/builder.rb +32 -0
  11. data/lib/coinsync/config.rb +94 -0
  12. data/lib/coinsync/crypto_classifier.rb +27 -0
  13. data/lib/coinsync/currencies.rb +35 -0
  14. data/lib/coinsync/currency_converter.rb +65 -0
  15. data/lib/coinsync/currency_converters/all.rb +1 -0
  16. data/lib/coinsync/currency_converters/base.rb +46 -0
  17. data/lib/coinsync/currency_converters/cache.rb +34 -0
  18. data/lib/coinsync/currency_converters/fixer.rb +36 -0
  19. data/lib/coinsync/currency_converters/nbp.rb +38 -0
  20. data/lib/coinsync/formatter.rb +44 -0
  21. data/lib/coinsync/import_task.rb +37 -0
  22. data/lib/coinsync/importers/all.rb +1 -0
  23. data/lib/coinsync/importers/ark_voting.rb +121 -0
  24. data/lib/coinsync/importers/base.rb +35 -0
  25. data/lib/coinsync/importers/binance_api.rb +210 -0
  26. data/lib/coinsync/importers/bitbay20.rb +144 -0
  27. data/lib/coinsync/importers/bitbay_api.rb +224 -0
  28. data/lib/coinsync/importers/bitcurex.rb +71 -0
  29. data/lib/coinsync/importers/bittrex_api.rb +81 -0
  30. data/lib/coinsync/importers/bittrex_csv.rb +75 -0
  31. data/lib/coinsync/importers/changelly.rb +57 -0
  32. data/lib/coinsync/importers/circle.rb +58 -0
  33. data/lib/coinsync/importers/default.rb +90 -0
  34. data/lib/coinsync/importers/etherdelta.rb +93 -0
  35. data/lib/coinsync/importers/kraken_api.rb +134 -0
  36. data/lib/coinsync/importers/kraken_common.rb +137 -0
  37. data/lib/coinsync/importers/kraken_csv.rb +28 -0
  38. data/lib/coinsync/importers/kucoin_api.rb +172 -0
  39. data/lib/coinsync/importers/lisk_voting.rb +110 -0
  40. data/lib/coinsync/outputs/all.rb +1 -0
  41. data/lib/coinsync/outputs/base.rb +32 -0
  42. data/lib/coinsync/outputs/list.rb +123 -0
  43. data/lib/coinsync/outputs/raw.rb +45 -0
  44. data/lib/coinsync/outputs/summary.rb +48 -0
  45. data/lib/coinsync/request.rb +31 -0
  46. data/lib/coinsync/run_command_task.rb +20 -0
  47. data/lib/coinsync/source.rb +43 -0
  48. data/lib/coinsync/source_filter.rb +10 -0
  49. data/lib/coinsync/table_printer.rb +29 -0
  50. data/lib/coinsync/transaction.rb +125 -0
  51. data/lib/coinsync/version.rb +3 -0
  52. metadata +95 -0
@@ -0,0 +1 @@
1
+ Dir[File.join(File.dirname(__FILE__), '*.rb')].each { |f| require(f) }
@@ -0,0 +1,32 @@
1
+ require_relative '../crypto_classifier'
2
+ require_relative '../formatter'
3
+
4
+ module CoinSync
5
+ module Outputs
6
+ def self.registered
7
+ @outputs ||= {}
8
+ end
9
+
10
+ class Base
11
+ def self.register_output(key)
12
+ if Outputs.registered[key]
13
+ raise "Output has already been registered at '#{key}'"
14
+ else
15
+ Outputs.registered[key] = self
16
+ end
17
+ end
18
+
19
+ def initialize(config, target_file)
20
+ @config = config
21
+ @target_file = target_file
22
+
23
+ @formatter = Formatter.new(config)
24
+ @classifier = CryptoClassifier.new(config)
25
+ end
26
+
27
+ def requires_currency_conversion?
28
+ false
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,123 @@
1
+ require 'csv'
2
+
3
+ require_relative 'base'
4
+
5
+ module CoinSync
6
+ module Outputs
7
+ class List < Base
8
+ register_output :list
9
+
10
+ def requires_currency_conversion?
11
+ true
12
+ end
13
+
14
+ def process_transactions(transactions, *args)
15
+ CSV.open(@target_file, 'w', col_sep: @config.column_separator) do |csv|
16
+ csv << headers
17
+
18
+ transactions.each do |tx|
19
+ csv << transaction_to_csv(tx)
20
+ end
21
+ end
22
+ end
23
+
24
+ def headers
25
+ line = [
26
+ 'No.',
27
+ 'Exchange',
28
+ 'Type',
29
+ 'Date',
30
+ 'Amount',
31
+ 'Asset',
32
+ 'Total value',
33
+ 'Price',
34
+ 'Currency'
35
+ ].map { |l| @config.translate(l) }
36
+
37
+ if currency = @config.convert_to_currency
38
+ line += [
39
+ @config.translate('Total value ($CURRENCY)').gsub('$CURRENCY', currency.code),
40
+ @config.translate('Price ($CURRENCY)').gsub('$CURRENCY', currency.code),
41
+ @config.translate('Exchange rate')
42
+ ]
43
+ end
44
+
45
+ line
46
+ end
47
+
48
+ def transaction_to_csv(tx)
49
+ if tx.purchase? || tx.sale?
50
+ fiat_transaction_to_csv(tx)
51
+ else
52
+ swap_transaction_to_csv(tx)
53
+ end
54
+ end
55
+
56
+ def fiat_transaction_to_csv(tx)
57
+ csv = [
58
+ tx.number || 0,
59
+ tx.exchange,
60
+ @config.translate(tx.type.to_s.capitalize),
61
+ @formatter.format_time(tx.time),
62
+ @formatter.format_crypto(tx.crypto_amount),
63
+ tx.crypto_currency.code,
64
+ @formatter.format_fiat(tx.fiat_amount),
65
+ @formatter.format_fiat_price(tx.price),
66
+ tx.fiat_currency.code || '–'
67
+ ]
68
+
69
+ if @config.convert_to_currency
70
+ if tx.converted
71
+ csv += [
72
+ @formatter.format_fiat(tx.converted.fiat_amount),
73
+ @formatter.format_fiat_price(tx.converted.price),
74
+ tx.converted.exchange_rate && @formatter.format_float(tx.converted.exchange_rate, precision: 4)
75
+ ]
76
+ else
77
+ csv += [
78
+ @formatter.format_fiat(tx.fiat_amount),
79
+ @formatter.format_fiat_price(tx.price),
80
+ nil
81
+ ]
82
+ end
83
+ end
84
+
85
+ csv
86
+ end
87
+
88
+ def swap_transaction_to_csv(tx)
89
+ if @classifier.is_purchase?(tx)
90
+ tx_type = Transaction::TYPE_PURCHASE
91
+ asset = tx.bought_currency
92
+ asset_amount = tx.bought_amount
93
+ currency = tx.sold_currency
94
+ currency_amount = tx.sold_amount
95
+ else
96
+ tx_type = Transaction::TYPE_SALE
97
+ asset = tx.sold_currency
98
+ asset_amount = tx.sold_amount
99
+ currency = tx.bought_currency
100
+ currency_amount = tx.bought_amount
101
+ end
102
+
103
+ csv = [
104
+ tx.number || 0,
105
+ tx.exchange,
106
+ @config.translate(tx_type.to_s.capitalize),
107
+ @formatter.format_time(tx.time),
108
+ @formatter.format_crypto(asset_amount),
109
+ asset.code,
110
+ @formatter.format_crypto(currency_amount),
111
+ @formatter.format_crypto(currency_amount / asset_amount),
112
+ currency.code
113
+ ]
114
+
115
+ if @config.convert_to_currency
116
+ csv += [nil, nil, nil]
117
+ end
118
+
119
+ csv
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,45 @@
1
+ require 'csv'
2
+
3
+ require_relative 'base'
4
+
5
+ module CoinSync
6
+ module Outputs
7
+ class Raw < Base
8
+ register_output :raw
9
+
10
+ def process_transactions(transactions, *args)
11
+ CSV.open(@target_file, 'w', col_sep: @config.column_separator) do |csv|
12
+ csv << headers
13
+
14
+ transactions.each do |tx|
15
+ csv << transaction_to_csv(tx)
16
+ end
17
+ end
18
+ end
19
+
20
+ def headers
21
+ [
22
+ 'Exchange',
23
+ 'Date',
24
+ 'Bought amount',
25
+ 'Bought currency',
26
+ 'Sold amount',
27
+ 'Sold currency'
28
+ ]
29
+ end
30
+
31
+ def transaction_to_csv(tx)
32
+ [
33
+ tx.exchange,
34
+ @formatter.format_time(tx.time),
35
+ tx.bought_currency.crypto? ?
36
+ @formatter.format_crypto(tx.bought_amount) : @formatter.format_fiat(tx.bought_amount),
37
+ tx.bought_currency.code,
38
+ tx.sold_currency.crypto? ?
39
+ @formatter.format_crypto(tx.sold_amount) : @formatter.format_fiat(tx.sold_amount),
40
+ tx.sold_currency.code
41
+ ]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ require 'bigdecimal'
2
+
3
+ require_relative 'base'
4
+ require_relative '../currencies'
5
+ require_relative '../table_printer'
6
+
7
+ module CoinSync
8
+ module Outputs
9
+ class Summary < Base
10
+ register_output :summary
11
+
12
+ def requires_currency_conversion?
13
+ false
14
+ end
15
+
16
+ def process_transactions(transactions, *args)
17
+ totals = Hash.new { BigDecimal(0) }
18
+
19
+ transactions.each do |tx|
20
+ if tx.bought_currency.crypto?
21
+ amount = totals[tx.bought_currency]
22
+ totals[tx.bought_currency] = amount + tx.bought_amount
23
+ end
24
+
25
+ if tx.sold_currency.crypto?
26
+ amount = totals[tx.sold_currency]
27
+ if amount >= tx.sold_amount
28
+ totals[tx.sold_currency] = amount - tx.sold_amount
29
+ else
30
+ raise "Summary: couldn't sell #{@formatter.format_crypto(tx.sold_amount)} #{tx.sold_currency.code} " +
31
+ "if only #{@formatter.format_crypto(amount)} was owned"
32
+ end
33
+ end
34
+ end
35
+
36
+ rows = totals.map do |currency, amount|
37
+ [
38
+ currency.code,
39
+ @formatter.format_crypto(amount)
40
+ ]
41
+ end
42
+
43
+ printer = TablePrinter.new
44
+ printer.print_table(['Coin', 'Amount'], rows, alignment: [:ljust, :rjust])
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ require_relative 'version'
5
+
6
+ module CoinSync
7
+ module Request
8
+ def self.get(url, &block)
9
+ self.request(url, Net::HTTP::Get, &block)
10
+ end
11
+
12
+ def self.post(url, &block)
13
+ self.request(url, Net::HTTP::Post, &block)
14
+ end
15
+
16
+ private
17
+
18
+ def self.request(url, request_type)
19
+ url = URI(url) if url.is_a?(String)
20
+
21
+ Net::HTTP.start(url.host, url.port, use_ssl: true) do |http|
22
+ request = request_type.new(url)
23
+ request['USER_AGENT'] = "coinsync/#{CoinSync::VERSION}"
24
+
25
+ yield request if block_given?
26
+
27
+ http.request(request)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'importers/all'
2
+
3
+ module CoinSync
4
+ class RunCommandTask
5
+ def initialize(config)
6
+ @config = config
7
+ end
8
+
9
+ def run(source_name, command, args = [])
10
+ source = @config.sources[source_name] or raise "Source not found in the config file: '#{source_name}'"
11
+ importer = source.importer
12
+
13
+ if importer.class.registered_commands.include?(command.to_sym)
14
+ importer.send(command.to_sym, *args)
15
+ else
16
+ raise "#{source_name}: no such command: #{command}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,43 @@
1
+ require_relative 'importers/all'
2
+
3
+ module CoinSync
4
+ class Source
5
+ attr_reader :key, :params, :filename
6
+
7
+ def initialize(config, key)
8
+ @config = config
9
+ @key = key
10
+
11
+ definition = config.source_definitions[key]
12
+
13
+ if definition.is_a?(Hash)
14
+ @params = definition
15
+ @filename = definition['file']
16
+ type = (definition['type'] || key).to_sym
17
+ elsif definition.is_a?(String)
18
+ @params = {}
19
+ @filename = definition
20
+ type = key.to_sym
21
+ elsif !config.source_definitions.has_key?(key)
22
+ raise "No such key in source list: '#{key}'"
23
+ else
24
+ raise "Unexpected source definition for '#{key}': #{definition}"
25
+ end
26
+
27
+ @importer_class = Importers.registered[type]
28
+
29
+ if @importer_class.nil?
30
+ if @params['type']
31
+ raise "Unknown source type for '#{key}': #{params['type']}"
32
+ else
33
+ raise "Unknown source type for '#{key}': please include a 'type' parameter " +
34
+ "or use a name of an existing importer"
35
+ end
36
+ end
37
+ end
38
+
39
+ def importer
40
+ @importer ||= @importer_class.new(@config, @params)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,10 @@
1
+ module CoinSync
2
+ class SourceFilter
3
+ def parse_command_line_args(arguments)
4
+ selected = arguments.select { |a| !a.start_with?('^') }
5
+ except = (arguments - selected).map { |a| a.gsub(/^\^/, '') }
6
+
7
+ [selected, except]
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,29 @@
1
+ module CoinSync
2
+ class TablePrinter
3
+ def print_table(header, rows, alignment: nil, separator: ' ')
4
+ rows.each do |row|
5
+ if row.length != header.length
6
+ raise "TablePrinter: All rows should have equal number of cells"
7
+ end
8
+
9
+ if !row.all? { |c| c.is_a?(String) }
10
+ raise "TablePrinter: All cells should be strings"
11
+ end
12
+ end
13
+
14
+ ids = (0...header.length)
15
+ widths = ids.map { |i| (rows + [header]).map { |r| r[i].length }.max }
16
+
17
+ puts ids.map { |i| header[i].center(widths[i]) }.join(separator)
18
+ puts '-' * (widths.inject(&:+) + separator.length * (header.length - 1))
19
+
20
+ rows.each do |row|
21
+ cells = ids.map do |i|
22
+ row[i].send(alignment && alignment[i] || :ljust, widths[i])
23
+ end
24
+
25
+ puts cells.join(separator)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,125 @@
1
+ require 'bigdecimal'
2
+
3
+ require_relative 'currencies'
4
+
5
+ module CoinSync
6
+ class Transaction
7
+ TYPE_PURCHASE = :purchase
8
+ TYPE_SALE = :sale
9
+ TYPE_SWAP = :swap
10
+
11
+ module Amounts
12
+ attr_reader :bought_currency, :sold_currency, :bought_amount, :sold_amount
13
+
14
+ def type
15
+ if bought_currency.crypto?
16
+ if sold_currency.crypto?
17
+ return TYPE_SWAP
18
+ else
19
+ return TYPE_PURCHASE
20
+ end
21
+ else
22
+ return TYPE_SALE
23
+ end
24
+ end
25
+
26
+ def purchase?
27
+ type == TYPE_PURCHASE
28
+ end
29
+
30
+ def sale?
31
+ type == TYPE_SALE
32
+ end
33
+
34
+ def swap?
35
+ type == TYPE_SWAP
36
+ end
37
+
38
+ def fiat_amount
39
+ case type
40
+ when TYPE_PURCHASE then sold_amount
41
+ when TYPE_SALE then bought_amount
42
+ else raise "Operation not supported for crypto swap transactions"
43
+ end
44
+ end
45
+
46
+ def crypto_amount
47
+ case type
48
+ when TYPE_PURCHASE then bought_amount
49
+ when TYPE_SALE then sold_amount
50
+ else raise "Operation not supported for crypto swap transactions"
51
+ end
52
+ end
53
+
54
+ def fiat_currency
55
+ case type
56
+ when TYPE_PURCHASE then sold_currency
57
+ when TYPE_SALE then bought_currency
58
+ else raise "Operation not supported for crypto swap transactions"
59
+ end
60
+ end
61
+
62
+ def crypto_currency
63
+ case type
64
+ when TYPE_PURCHASE then bought_currency
65
+ when TYPE_SALE then sold_currency
66
+ else raise "Operation not supported for crypto swap transactions"
67
+ end
68
+ end
69
+
70
+ def price
71
+ fiat_amount / crypto_amount
72
+ end
73
+ end
74
+
75
+ class ConvertedAmounts
76
+ include Amounts
77
+ attr_writer :bought_currency, :sold_currency, :bought_amount, :sold_amount
78
+ attr_accessor :exchange_rate
79
+ end
80
+
81
+ attr_reader :exchange, :time
82
+ attr_accessor :number, :converted
83
+
84
+ include Amounts
85
+
86
+ def initialize(number: nil, exchange:, bought_currency:, sold_currency:, time:, bought_amount:, sold_amount:)
87
+ @number = number
88
+ @exchange = exchange
89
+
90
+ if time.is_a?(Time)
91
+ @time = time.getlocal
92
+ else
93
+ raise "Transaction: '#{time}' is not a valid Time object"
94
+ end
95
+
96
+ if bought_amount.is_a?(BigDecimal)
97
+ @bought_amount = bought_amount
98
+ else
99
+ raise "Transaction: '#{bought_amount}' should be a BigDecimal"
100
+ end
101
+
102
+ if bought_currency.is_a?(Currency)
103
+ @bought_currency = bought_currency
104
+ else
105
+ raise "Transaction: '#{bought_currency}' is not a valid currency"
106
+ end
107
+
108
+ (bought_amount > 0) or raise "Transaction: bought_amount should be positive (#{bought_amount})"
109
+
110
+ if sold_amount.is_a?(BigDecimal)
111
+ @sold_amount = sold_amount
112
+ else
113
+ raise "Transaction: '#{sold_amount}' should be a BigDecimal"
114
+ end
115
+
116
+ if sold_currency.is_a?(Currency) || sold_amount == 0
117
+ @sold_currency = sold_currency
118
+ else
119
+ raise "Transaction: '#{sold_currency}' is not a valid currency"
120
+ end
121
+
122
+ (sold_amount >= 0) or raise "Transaction: sold_amount should not be negative (#{sold_amount})"
123
+ end
124
+ end
125
+ end