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