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
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