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