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.
- checksums.yaml +7 -0
- data/MIT-LICENSE.txt +21 -0
- data/README.md +255 -0
- data/bin/coinsync +78 -0
- data/doc/importers.md +201 -0
- data/lib/coinsync.rb +18 -0
- data/lib/coinsync/balance.rb +19 -0
- data/lib/coinsync/balance_task.rb +65 -0
- data/lib/coinsync/build_task.rb +43 -0
- data/lib/coinsync/builder.rb +32 -0
- data/lib/coinsync/config.rb +94 -0
- data/lib/coinsync/crypto_classifier.rb +27 -0
- data/lib/coinsync/currencies.rb +35 -0
- data/lib/coinsync/currency_converter.rb +65 -0
- data/lib/coinsync/currency_converters/all.rb +1 -0
- data/lib/coinsync/currency_converters/base.rb +46 -0
- data/lib/coinsync/currency_converters/cache.rb +34 -0
- data/lib/coinsync/currency_converters/fixer.rb +36 -0
- data/lib/coinsync/currency_converters/nbp.rb +38 -0
- data/lib/coinsync/formatter.rb +44 -0
- data/lib/coinsync/import_task.rb +37 -0
- data/lib/coinsync/importers/all.rb +1 -0
- data/lib/coinsync/importers/ark_voting.rb +121 -0
- data/lib/coinsync/importers/base.rb +35 -0
- data/lib/coinsync/importers/binance_api.rb +210 -0
- data/lib/coinsync/importers/bitbay20.rb +144 -0
- data/lib/coinsync/importers/bitbay_api.rb +224 -0
- data/lib/coinsync/importers/bitcurex.rb +71 -0
- data/lib/coinsync/importers/bittrex_api.rb +81 -0
- data/lib/coinsync/importers/bittrex_csv.rb +75 -0
- data/lib/coinsync/importers/changelly.rb +57 -0
- data/lib/coinsync/importers/circle.rb +58 -0
- data/lib/coinsync/importers/default.rb +90 -0
- data/lib/coinsync/importers/etherdelta.rb +93 -0
- data/lib/coinsync/importers/kraken_api.rb +134 -0
- data/lib/coinsync/importers/kraken_common.rb +137 -0
- data/lib/coinsync/importers/kraken_csv.rb +28 -0
- data/lib/coinsync/importers/kucoin_api.rb +172 -0
- data/lib/coinsync/importers/lisk_voting.rb +110 -0
- data/lib/coinsync/outputs/all.rb +1 -0
- data/lib/coinsync/outputs/base.rb +32 -0
- data/lib/coinsync/outputs/list.rb +123 -0
- data/lib/coinsync/outputs/raw.rb +45 -0
- data/lib/coinsync/outputs/summary.rb +48 -0
- data/lib/coinsync/request.rb +31 -0
- data/lib/coinsync/run_command_task.rb +20 -0
- data/lib/coinsync/source.rb +43 -0
- data/lib/coinsync/source_filter.rb +10 -0
- data/lib/coinsync/table_printer.rb +29 -0
- data/lib/coinsync/transaction.rb +125 -0
- data/lib/coinsync/version.rb +3 -0
- 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
|