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
data/lib/coinsync.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "coinsync/balance"
2
+ require "coinsync/balance_task"
3
+ require "coinsync/build_task"
4
+ require "coinsync/builder"
5
+ require "coinsync/config"
6
+ require "coinsync/crypto_classifier"
7
+ require "coinsync/currencies"
8
+ require "coinsync/currency_converter"
9
+ require "coinsync/formatter"
10
+ require "coinsync/import_task"
11
+ require "coinsync/request"
12
+ require "coinsync/run_command_task"
13
+ require "coinsync/source"
14
+ require "coinsync/transaction"
15
+ require "coinsync/version"
16
+
17
+ module CoinSync
18
+ end
@@ -0,0 +1,19 @@
1
+ module CoinSync
2
+ class Balance
3
+ attr_reader :currency, :available, :locked
4
+
5
+ def initialize(currency, available: BigDecimal(0), locked: BigDecimal(0))
6
+ @currency = currency
7
+ @available = available
8
+ @locked = locked
9
+ end
10
+
11
+ def +(balance)
12
+ return Balance.new(
13
+ @currency,
14
+ available: @available + balance.available,
15
+ locked: @locked + balance.locked
16
+ )
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,65 @@
1
+ require_relative 'balance'
2
+ require_relative 'formatter'
3
+ require_relative 'importers/all'
4
+
5
+ module CoinSync
6
+ class BalanceTask
7
+ def initialize(config)
8
+ @config = config
9
+ @formatter = Formatter.new(config)
10
+ end
11
+
12
+ def run(selected = nil, except = nil)
13
+ balances = {}
14
+ columns = []
15
+ rows = []
16
+
17
+ @config.filtered_sources(selected, except).each do |key, source|
18
+ importer = source.importer
19
+
20
+ if importer.respond_to?(:can_import?)
21
+ if importer.can_import?(:balances)
22
+ print "[#{key}] Importing balances... "
23
+
24
+ columns << key
25
+
26
+ importer.import_balances.each do |balance|
27
+ balances[balance.currency] ||= {}
28
+ balances[balance.currency][key] = balance
29
+ balances[balance.currency][nil] ||= Balance.new(balance.currency)
30
+ balances[balance.currency][nil] += balance
31
+ end
32
+
33
+ puts "√"
34
+ else
35
+ puts "[#{key}] Skipping import"
36
+ end
37
+ end
38
+ end
39
+
40
+ columns.sort!
41
+
42
+ balances.keys.sort.each do |coin|
43
+ row = [coin.code, '|']
44
+ row += columns.map { |e|
45
+ available = balances[coin][e]&.available
46
+ locked = balances[coin][e]&.locked
47
+ available ? @formatter.format_crypto(available) + (locked > 0 ? ' (+)' : '') : ''
48
+ }
49
+ row << '|'
50
+ row << @formatter.format_crypto(balances[coin][nil].available)
51
+ rows << row
52
+ end
53
+
54
+ puts
55
+
56
+ printer = TablePrinter.new
57
+ printer.print_table(
58
+ ['Coin', '|'] + columns + ['|', 'TOTAL'],
59
+ rows,
60
+ alignment: [:ljust, :center] + columns.map { |e| :rjust } + [:center, :rjust],
61
+ separator: ' '
62
+ )
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,43 @@
1
+ require 'fileutils'
2
+
3
+ require_relative 'builder'
4
+ require_relative 'currency_converter'
5
+ require_relative 'outputs/all'
6
+
7
+ module CoinSync
8
+ class BuildTask
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ def run(output_name, args = [])
14
+ if output_name.nil?
15
+ puts "Error: Build task name not given"
16
+ exit 1
17
+ end
18
+
19
+ output_class = Outputs.registered[output_name.to_sym]
20
+
21
+ if output_class.nil?
22
+ puts "Unknown build task: #{output_name}"
23
+ exit 1
24
+ end
25
+
26
+ FileUtils.mkdir_p 'build'
27
+
28
+ builder = Builder.new(@config)
29
+ transactions = builder.build_transaction_list
30
+
31
+ output = output_class.new(@config, "build/#{output_name}.csv")
32
+
33
+ if output.requires_currency_conversion?
34
+ if @config.convert_to_currency
35
+ converter = CurrencyConverter.new(@config)
36
+ converter.process_transactions(transactions)
37
+ end
38
+ end
39
+
40
+ output.process_transactions(transactions, *args)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'importers/all'
2
+
3
+ module CoinSync
4
+ class Builder
5
+ attr_reader :transactions
6
+
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+
11
+ def build_transaction_list
12
+ transactions = []
13
+
14
+ @config.sources.each do |key, source|
15
+ if source.importer.can_build?
16
+ if source.filename.nil?
17
+ raise "No filename specified for '#{key}', please add a 'file' parameter."
18
+ end
19
+
20
+ File.open(source.filename, 'r') do |file|
21
+ transactions.concat(source.importer.read_transaction_list(file))
22
+ end
23
+ end
24
+ end
25
+
26
+ transactions.each_with_index { |tx, i| tx.number = i + 1 }
27
+
28
+ @transactions = transactions.sort_by { |tx| [tx.time, tx.number] }
29
+ @transactions.each_with_index { |tx, i| tx.number = i + 1 }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,94 @@
1
+ require 'yaml'
2
+
3
+ require_relative 'source'
4
+
5
+ module CoinSync
6
+ class Config
7
+ attr_reader :source_definitions, :settings
8
+
9
+ DEFAULT_CONFIG = 'config.yml'
10
+
11
+ def self.load_from_file(filename = nil)
12
+ yaml = YAML.load(File.read(filename || DEFAULT_CONFIG))
13
+ self.new(yaml, filename)
14
+ end
15
+
16
+ def initialize(yaml, config_path = nil)
17
+ @source_definitions = yaml['sources'] or raise 'Config: No sources listed'
18
+ @settings = yaml['settings'] || {}
19
+ @labels = @settings['labels'] || {}
20
+
21
+ if includes = yaml['include']
22
+ includes.each do |file|
23
+ directory = config_path ? [config_path, '..'] : ['.']
24
+ require(File.expand_path(File.join(*directory, file)))
25
+ end
26
+ end
27
+
28
+ set_timezone(timezone) if timezone
29
+ end
30
+
31
+ def sources
32
+ @sources ||= Hash[@source_definitions.keys.map { |key| [key, Source.new(self, key)] }]
33
+ end
34
+
35
+ def filtered_sources(selected, except = nil)
36
+ included = if selected.nil? || selected.empty?
37
+ sources.values
38
+ else
39
+ selected = [selected] unless selected.is_a?(Array)
40
+
41
+ selected.map do |key|
42
+ sources[key] or raise "Source not found in the config file: '#{key}'"
43
+ end
44
+ end
45
+
46
+ if except
47
+ except = [except] unless except.is_a?(Array)
48
+ included -= except.map { |key| sources[key] }
49
+ end
50
+
51
+ Hash[included.map { |source| [source.key, source] }]
52
+ end
53
+
54
+ def set_timezone(timezone)
55
+ ENV['TZ'] = timezone
56
+ end
57
+
58
+ def base_cryptocurrencies
59
+ settings['base_cryptocurrencies'] || ['USDT', 'BTC', 'ETH', 'BNB', 'KCS', 'LTC', 'BCH', 'NEO']
60
+ end
61
+
62
+ def column_separator
63
+ settings['column_separator'] || ','
64
+ end
65
+
66
+ def decimal_separator
67
+ custom_decimal_separator || '.'
68
+ end
69
+
70
+ def custom_decimal_separator
71
+ settings['decimal_separator']
72
+ end
73
+
74
+ def convert_to_currency
75
+ settings['convert_to'] ? FiatCurrency.new(settings['convert_to']) : nil
76
+ end
77
+
78
+ def currency_converter
79
+ settings['convert_with']&.to_sym || :fixer
80
+ end
81
+
82
+ def time_format
83
+ settings['time_format']
84
+ end
85
+
86
+ def timezone
87
+ settings['timezone']
88
+ end
89
+
90
+ def translate(label)
91
+ @labels[label] || label
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'currencies'
2
+
3
+ module CoinSync
4
+ class CryptoClassifier
5
+ MAX_INDEX = 1_000_000
6
+
7
+ def initialize(config)
8
+ @config = config
9
+ @base_currencies = config.base_cryptocurrencies.map { |c| CryptoCurrency.new(c) }
10
+ end
11
+
12
+ def is_purchase?(transaction)
13
+ bought_index = @base_currencies.index(transaction.bought_currency) || MAX_INDEX
14
+ sold_index = @base_currencies.index(transaction.sold_currency) || MAX_INDEX
15
+
16
+ if bought_index < sold_index
17
+ false
18
+ elsif bought_index > sold_index
19
+ true
20
+ else
21
+ raise "Couldn't determine which cryptocurrency is the base one: #{transaction.bought_currency.code} vs " +
22
+ "#{transaction.sold_currency.code}. Use the `base_cryptocurrencies` setting to explicitly choose " +
23
+ "base cryptocurrencies."
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+ module CoinSync
2
+ class Currency < Struct.new(:code)
3
+ def <=>(other)
4
+ self.code <=> other.code
5
+ end
6
+ end
7
+
8
+ class FiatCurrency < Currency
9
+ def fiat?
10
+ true
11
+ end
12
+
13
+ def crypto?
14
+ false
15
+ end
16
+ end
17
+
18
+ class CryptoCurrency < Currency
19
+ MAPPING = {
20
+ 'XRB' => 'NANO'
21
+ }
22
+
23
+ def initialize(code)
24
+ super(MAPPING[code] || code)
25
+ end
26
+
27
+ def fiat?
28
+ false
29
+ end
30
+
31
+ def crypto?
32
+ true
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,65 @@
1
+ require 'bigdecimal'
2
+ require 'time'
3
+
4
+ require_relative 'currencies'
5
+ require_relative 'currency_converters/all'
6
+ require_relative 'transaction'
7
+
8
+ 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
21
+ end
22
+
23
+ def process_transactions(transactions)
24
+ transactions.each do |tx|
25
+ print '.'
26
+
27
+ if tx.bought_currency.fiat? && tx.bought_currency != @target_currency
28
+ 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
+ tx.converted.sold_currency = tx.sold_currency
38
+ tx.converted.sold_amount = tx.sold_amount
39
+ elsif tx.sold_currency.fiat? && tx.sold_currency != @target_currency
40
+ tx.converted = Transaction::ConvertedAmounts.new
41
+ tx.converted.bought_currency = tx.bought_currency
42
+ tx.converted.bought_amount = tx.bought_amount
43
+ tx.converted.sold_currency = @target_currency
44
+
45
+ if tx.sold_currency.code
46
+ tx.converted.exchange_rate = @converter.convert(
47
+ BigDecimal.new(1),
48
+ from: tx.sold_currency,
49
+ to: @target_currency,
50
+ date: tx.time.to_date
51
+ )
52
+ tx.converted.sold_amount = tx.sold_amount * tx.converted.exchange_rate
53
+ else
54
+ tx.converted.exchange_rate = nil
55
+ tx.converted.sold_amount = BigDecimal.new(0)
56
+ end
57
+ end
58
+ end
59
+
60
+ @converter.finalize
61
+
62
+ puts
63
+ end
64
+ end
65
+ end
@@ -0,0 +1 @@
1
+ Dir[File.join(File.dirname(__FILE__), '*.rb')].each { |f| require(f) }
@@ -0,0 +1,46 @@
1
+ require 'bigdecimal'
2
+
3
+ require_relative 'cache'
4
+ require_relative '../currencies'
5
+
6
+ module CoinSync
7
+ module CurrencyConverters
8
+ def self.registered
9
+ @converters ||= {}
10
+ end
11
+
12
+ class Base
13
+ def self.register_converter(key)
14
+ if CurrencyConverters.registered[key]
15
+ raise "Currency converter has already been registered at '#{key}'"
16
+ else
17
+ CurrencyConverters.registered[key] = self
18
+ end
19
+ end
20
+
21
+ def initialize
22
+ @cache = Cache.new(self.class.name.downcase.split('::').last)
23
+ end
24
+
25
+ def convert(amount, from:, to:, date:)
26
+ (amount > 0) or raise "#{self.class}: amount should be positive"
27
+ (amount.is_a?(BigDecimal)) or raise "#{self.class}: 'amount' should be a BigDecimal"
28
+ (from.is_a?(FiatCurrency)) or raise "#{self.class}: 'from' should be a FiatCurrency"
29
+ (to.is_a?(FiatCurrency)) or raise "#{self.class}: 'to' should be a FiatCurrency"
30
+ (date.is_a?(Date)) or raise "#{self.class}: 'date' should be a Date"
31
+
32
+ if rate = @cache[from, to, date]
33
+ return rate * amount
34
+ else
35
+ rate = fetch_conversion_rate(from: from, to: to, date: date)
36
+ @cache[from, to, date] = rate
37
+ return rate * amount
38
+ end
39
+ end
40
+
41
+ def finalize
42
+ @cache.save
43
+ end
44
+ end
45
+ end
46
+ end