coinsync 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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