coinsync 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5ee9560e9255a67e3389a7aa39ca48820dffe803
4
- data.tar.gz: e946c4c12142d887c53f50b54c85d4a1d473f5bf
3
+ metadata.gz: c0072156912886d0dda428c95b5421b230ce73bb
4
+ data.tar.gz: 3cb523a19af17908648fad02113d63fea9c7cb47
5
5
  SHA512:
6
- metadata.gz: cff97b38cecbc4933545d1f50525ab9aeba5e02ac13474e103e3108174b175bd98b6536097bec5816f4c265504fb155bc8d36081cdfdd6ef3f0987acbd5fd3f3
7
- data.tar.gz: c324c154236885613861372a8302582dd80214416eaa3e85690826a0864ee43c610f95869c78c0e174e8ae9d882f176657bb476909a14d5cef808471abfad6d5
6
+ metadata.gz: 57fe540967120d3926ad5ac5e7883aae9f49147b6e6eba82e0f4b1c38aa1ea54af70adfd1140545e75fa1255907b9bd4a6860094dd75f9f77e96d78d227a52ce
7
+ data.tar.gz: 4aa898c645ca40f083720d22b517e7d3dc8c53ab8246bf977c5fbe00dccbc8188c7b44e34b220c119076b9597a373ba031e6c8bc5a047002c0923ea8b9f7d272
@@ -1,4 +1,5 @@
1
1
  require 'ostruct'
2
+ require 'tzinfo'
2
3
  require 'yaml'
3
4
 
4
5
  require_relative 'currencies'
@@ -28,8 +29,6 @@ module CoinSync
28
29
  require(File.expand_path(File.join(*directory, file)))
29
30
  end
30
31
  end
31
-
32
- set_timezone(timezone) if timezone
33
32
  end
34
33
 
35
34
  def sources
@@ -55,10 +54,6 @@ module CoinSync
55
54
  Hash[included.map { |source| [source.key, source] }]
56
55
  end
57
56
 
58
- def set_timezone(timezone)
59
- ENV['TZ'] = timezone
60
- end
61
-
62
57
  def base_cryptocurrencies
63
58
  settings['base_cryptocurrencies'] || ['USDT', 'BTC', 'ETH', 'BNB', 'KCS', 'LTC', 'BCH', 'NEO']
64
59
  end
@@ -88,7 +83,7 @@ module CoinSync
88
83
  end
89
84
 
90
85
  def timezone
91
- settings['timezone']
86
+ settings['timezone'] && TZInfo::Timezone.get(settings['timezone'])
92
87
  end
93
88
 
94
89
  def translate(label)
@@ -26,7 +26,7 @@ module CoinSync
26
26
  BigDecimal.new(1),
27
27
  from: tx.bought_currency,
28
28
  to: @target_currency,
29
- date: tx.time.to_date
29
+ time: tx.time
30
30
  )
31
31
  tx.converted.bought_amount = tx.bought_amount * tx.converted.exchange_rate
32
32
  else
@@ -44,7 +44,7 @@ module CoinSync
44
44
  BigDecimal.new(1),
45
45
  from: tx.sold_currency,
46
46
  to: @target_currency,
47
- date: tx.time.to_date
47
+ time: tx.time
48
48
  )
49
49
  tx.converted.sold_amount = tx.sold_amount * tx.converted.exchange_rate
50
50
  else
@@ -23,20 +23,17 @@ module CoinSync
23
23
  @cache = Cache.new(self.class.name.downcase.split('::').last)
24
24
  end
25
25
 
26
- def convert(amount, from:, to:, date:)
26
+ def convert(amount, from:, to:, time:)
27
27
  (amount > 0) or raise "#{self.class}: amount should be positive"
28
28
  (amount.is_a?(BigDecimal)) or raise "#{self.class}: 'amount' should be a BigDecimal"
29
- (from.is_a?(FiatCurrency)) or raise "#{self.class}: 'from' should be a FiatCurrency"
30
- (to.is_a?(FiatCurrency)) or raise "#{self.class}: 'to' should be a FiatCurrency"
31
- (date.is_a?(Date)) or raise "#{self.class}: 'date' should be a Date"
32
29
 
33
- if rate = @cache[from, to, date]
34
- return rate * amount
35
- else
36
- rate = fetch_conversion_rate(from: from, to: to, date: date)
37
- @cache[from, to, date] = rate
38
- return rate * amount
39
- end
30
+ rate = get_conversion_rate(from: from, to: to, time: time)
31
+
32
+ rate * amount
33
+ end
34
+
35
+ def get_conversion_rate(from:, to:, time:)
36
+ raise "not implemented"
40
37
  end
41
38
 
42
39
  def finalize
@@ -10,12 +10,23 @@ module CoinSync
10
10
  register_converter :exchangeratesapi
11
11
 
12
12
  BASE_URL = "https://exchangeratesapi.io/api"
13
+ ECB_TIMEZONE = TZInfo::Timezone.get('Europe/Berlin')
13
14
 
14
15
  class Exception < StandardError; end
15
16
  class NoDataException < Exception; end
16
17
  class BadRequestException < Exception; end
17
18
 
18
- def fetch_conversion_rate(from:, to:, date:)
19
+ def get_conversion_rate(from:, to:, time:)
20
+ (from.is_a?(FiatCurrency)) or raise "#{self.class}: 'from' should be a FiatCurrency"
21
+ (to.is_a?(FiatCurrency)) or raise "#{self.class}: 'to' should be a FiatCurrency"
22
+ (time.is_a?(Time)) or raise "#{self.class}: 'time' should be a Time"
23
+
24
+ date = ECB_TIMEZONE.utc_to_local(time.utc).to_date
25
+
26
+ if rate = @cache[from, to, date]
27
+ return rate
28
+ end
29
+
19
30
  response = Request.get("#{BASE_URL}/#{date}?base=#{from.code}")
20
31
 
21
32
  case response
@@ -24,6 +35,8 @@ module CoinSync
24
35
  rate = json['rates'][to.code.upcase]
25
36
  raise NoDataException.new("No exchange rate found for #{to.code.upcase}") if rate.nil?
26
37
 
38
+ @cache[from, to, date] = rate
39
+
27
40
  return rate
28
41
  when Net::HTTPBadRequest
29
42
  raise BadRequestException.new(response)
@@ -1,5 +1,6 @@
1
1
  require 'json'
2
2
  require 'net/http'
3
+ require 'tzinfo'
3
4
 
4
5
  require_relative 'base'
5
6
  require_relative '../request'
@@ -10,14 +11,25 @@ module CoinSync
10
11
  register_converter :nbp
11
12
 
12
13
  BASE_URL = "https://api.nbp.pl/api"
14
+ POLISH_TIMEZONE = TZInfo::Timezone.get('Europe/Warsaw')
13
15
 
14
16
  class Exception < StandardError; end
15
17
  class NoDataException < Exception; end
16
18
  class BadRequestException < Exception; end
17
19
 
18
- def fetch_conversion_rate(from:, to:, date:)
20
+ def get_conversion_rate(from:, to:, time:)
21
+ (from.is_a?(FiatCurrency)) or raise "#{self.class}: 'from' should be a FiatCurrency"
22
+ (to.is_a?(FiatCurrency)) or raise "#{self.class}: 'to' should be a FiatCurrency"
23
+ (time.is_a?(Time)) or raise "#{self.class}: 'time' should be a Time"
24
+
19
25
  raise "Only conversions to PLN are supported" if to.code != 'PLN'
20
26
 
27
+ date = POLISH_TIMEZONE.utc_to_local(time.utc).to_date
28
+
29
+ if rate = @cache[from, to, date]
30
+ return rate
31
+ end
32
+
21
33
  response = Request.get("#{BASE_URL}/exchangerates/rates/a/#{from.code}/#{date - 8}/#{date - 1}/?format=json")
22
34
 
23
35
  case response
@@ -26,6 +38,8 @@ module CoinSync
26
38
  rate = json['rates'] && json['rates'].last && json['rates'].last['mid']
27
39
  raise NoDataException.new("No exchange rate found for #{from.code.upcase}") if rate.nil?
28
40
 
41
+ @cache[from, to, date] = rate
42
+
29
43
  return rate
30
44
  when Net::HTTPBadRequest
31
45
  raise BadRequestException.new(response)
@@ -33,7 +33,8 @@ module CoinSync
33
33
  end
34
34
 
35
35
  def format_time(time)
36
- time.strftime(@config.time_format || '%Y-%m-%d %H:%M:%S')
36
+ local_time = @config.timezone ? @config.timezone.utc_to_local(time.utc) : time
37
+ local_time.strftime(@config.time_format || '%Y-%m-%d %H:%M:%S')
37
38
  end
38
39
 
39
40
  def parse_decimal(string)
@@ -68,23 +68,30 @@ module CoinSync
68
68
  transactions = []
69
69
 
70
70
  @traded_pairs.uniq.each do |pair|
71
- response = make_request('/v3/myTrades', limit: 500, symbol: pair) # TODO: paging
72
-
73
- case response
74
- when Net::HTTPSuccess
75
- json = JSON.parse(response.body)
76
-
77
- if json.is_a?(Hash)
78
- raise "Binance importer: Invalid response: #{response.body}"
71
+ lastId = 0
72
+
73
+ loop do
74
+ response = make_request('/v3/myTrades', limit: 500, fromId: lastId + 1, symbol: pair)
75
+
76
+ case response
77
+ when Net::HTTPSuccess
78
+ json = JSON.parse(response.body)
79
+
80
+ if !json.is_a?(Array)
81
+ raise "Binance importer: Invalid response: #{response.body}"
82
+ elsif json.empty?
83
+ break
84
+ else
85
+ json.each { |tx| tx['symbol'] = pair }
86
+ lastId = json.map { |j| j['id'] }.sort.last
87
+
88
+ transactions.concat(json)
89
+ end
90
+ when Net::HTTPBadRequest
91
+ raise "Binance importer: Bad request: #{response} (#{response.body})"
92
+ else
93
+ raise "Binance importer: Bad response: #{response}"
79
94
  end
80
-
81
- json.each { |tx| tx['symbol'] = pair }
82
-
83
- transactions.concat(json)
84
- when Net::HTTPBadRequest
85
- raise "Binance importer: Bad request: #{response} (#{response.body})"
86
- else
87
- raise "Binance importer: Bad response: #{response}"
88
95
  end
89
96
  end
90
97
 
@@ -187,6 +194,8 @@ module CoinSync
187
194
  private
188
195
 
189
196
  def make_request(path, params = {}, signed = true)
197
+ print '.'
198
+
190
199
  if signed
191
200
  (@api_key && @secret_key) or raise "Public and secret API keys must be provided"
192
201
 
@@ -1,6 +1,7 @@
1
1
  require 'bigdecimal'
2
2
  require 'csv'
3
3
  require 'time'
4
+ require 'tzinfo'
4
5
 
5
6
  require_relative 'base'
6
7
  require_relative '../currencies'
@@ -24,10 +25,12 @@ module CoinSync
24
25
  class HistoryEntry
25
26
  attr_accessor :date, :accounting_date, :type, :amount, :currency
26
27
 
28
+ POLISH_TIMEZONE = TZInfo::Timezone.get('Europe/Warsaw')
29
+
27
30
  def initialize(line)
28
- # TODO: force parsing in Polish timezone
29
- @date = Time.parse(line[0]) unless line[0] == '-'
30
- @accounting_date = Time.parse(line[1]) unless line[1] == '-'
31
+ @date = POLISH_TIMEZONE.local_to_utc(Time.parse(line[0])) unless line[0] == '-'
32
+ @accounting_date = POLISH_TIMEZONE.local_to_utc(Time.parse(line[1])) unless line[1] == '-'
33
+
31
34
  @type = line[2]
32
35
 
33
36
  amount, currency = line[3].split(' ')
@@ -3,6 +3,7 @@ require 'json'
3
3
  require 'net/http'
4
4
  require 'openssl'
5
5
  require 'time'
6
+ require 'tzinfo'
6
7
  require 'uri'
7
8
 
8
9
  require_relative 'base'
@@ -22,14 +23,16 @@ module CoinSync
22
23
  OP_SALE = '-pay_for_currency'
23
24
  OP_FEE = '-fee'
24
25
 
25
- MAX_TIME_DIFFERENCE = 2.0 # TODO: this breaks too easily (3.0)
26
+ MAX_TIME_DIFFERENCE = 5.0
26
27
  TRANSACTION_TYPES = [OP_PURCHASE, OP_SALE, OP_FEE]
27
28
 
29
+ POLISH_TIMEZONE = TZInfo::Timezone.get('Europe/Warsaw')
30
+
28
31
  class HistoryEntry
29
32
  attr_accessor :date, :amount, :type, :currency
30
33
 
31
34
  def initialize(hash)
32
- @date = Time.parse(hash['time']) # TODO: these times are all fucked up
35
+ @date = POLISH_TIMEZONE.local_to_utc(Time.parse(hash['time']))
33
36
  @amount = BigDecimal.new(hash['amount'])
34
37
  @type = hash['operation_type']
35
38
  @currency = parse_currency(hash['currency'])
@@ -94,7 +97,8 @@ module CoinSync
94
97
  currencies.each do |currency|
95
98
  sleep 1 # rate limiting
96
99
 
97
- response = make_request('history', currency: currency, limit: 10000) # TODO: does this limit really work?
100
+ # TODO: does this limit really work? (no way to test it really and docs don't mention a max value)
101
+ response = make_request('history', currency: currency, limit: 10000)
98
102
 
99
103
  case response
100
104
  when Net::HTTPSuccess
@@ -144,15 +148,17 @@ module CoinSync
144
148
 
145
149
  next unless TRANSACTION_TYPES.include?(entry.type)
146
150
 
147
- if !matching.empty? && matching.any? { |e| (e.date - entry.date).abs > MAX_TIME_DIFFERENCE }
148
- transactions << process_matched(matching)
151
+ if !matching.empty?
152
+ must_match = matching.any? { |e| (e.date - entry.date).abs > MAX_TIME_DIFFERENCE }
153
+ transaction = process_matched(matching, must_match)
154
+ transactions << transaction if transaction
149
155
  end
150
156
 
151
157
  matching << entry
152
158
  end
153
159
 
154
160
  if !matching.empty?
155
- transactions << process_matched(matching)
161
+ transactions << process_matched(matching, true)
156
162
  end
157
163
 
158
164
  transactions
@@ -161,7 +167,7 @@ module CoinSync
161
167
 
162
168
  private
163
169
 
164
- def process_matched(matching)
170
+ def process_matched(matching, must_match)
165
171
  if matching.length % 3 == 0
166
172
  purchases = matching.select { |tx| tx.type == OP_PURCHASE }
167
173
  sales = matching.select { |tx| tx.type == OP_SALE }
@@ -186,7 +192,10 @@ module CoinSync
186
192
  end
187
193
  end
188
194
 
189
- raise "BitBay API importer error: Couldn't match some history lines: #{matching}"
195
+ if must_match
196
+ raise "BitBay API importer error: Couldn't match some history lines: " +
197
+ matching.map { |m| "\n#{m.inspect}" }.join
198
+ end
190
199
  end
191
200
 
192
201
  def make_request(method, params = {})
@@ -1,6 +1,7 @@
1
1
  require 'bigdecimal'
2
2
  require 'csv'
3
3
  require 'time'
4
+ require 'tzinfo'
4
5
 
5
6
  require_relative 'base'
6
7
  require_relative '../currencies'
@@ -14,11 +15,12 @@ module CoinSync
14
15
  class HistoryEntry
15
16
  attr_accessor :lp, :type, :date, :market, :amount, :price, :total, :fee, :fee_currency, :id
16
17
 
18
+ POLISH_TIMEZONE = TZInfo::Timezone.get('Europe/Warsaw')
19
+
17
20
  def initialize(line)
18
- # TODO: force parsing in Polish timezone
19
21
  @lp = line[0].to_i
20
22
  @type = line[1]
21
- @date = Time.parse(line[2])
23
+ @date = POLISH_TIMEZONE.local_to_utc(Time.parse(line[2]))
22
24
  @market = FiatCurrency.new(line[3])
23
25
  @amount = BigDecimal.new(line[4])
24
26
  @price = BigDecimal.new(line[5].split(' ').first)
@@ -0,0 +1,73 @@
1
+ require 'bigdecimal'
2
+ require 'csv'
3
+ require 'time'
4
+
5
+ require_relative 'base'
6
+ require_relative '../currencies'
7
+ require_relative '../transaction'
8
+
9
+ module CoinSync
10
+ module Importers
11
+ class BitstampCSV < Base
12
+ register_importer :bitstamp_csv
13
+
14
+ class HistoryEntry
15
+ attr_accessor :lp, :type, :date, :market, :amount, :price, :total, :fee, :fee_currency, :id
16
+
17
+ def initialize(line)
18
+ @type = line[0]
19
+ @date = Time.parse(line[1])
20
+ @account = line[2]
21
+
22
+
23
+ @lp = line[0].to_i
24
+ @market = FiatCurrency.new(line[3])
25
+ @amount = BigDecimal.new(line[4])
26
+ @price = BigDecimal.new(line[5].split(' ').first)
27
+ @total = BigDecimal.new(line[6].split(' ').first)
28
+ @fee = BigDecimal.new(line[7].split(' ').first)
29
+ @fee_currency = line[7].split(' ').last
30
+ @id = line[8].to_i
31
+ end
32
+ end
33
+
34
+ def read_transaction_list(source)
35
+ csv = CSV.new(source, col_sep: ',')
36
+
37
+ transactions = []
38
+ bitcoin = CryptoCurrency.new('BTC')
39
+
40
+ csv.each do |line|
41
+ next if line.empty?
42
+ next if line[0] == 'Type'
43
+
44
+ entry = HistoryEntry.new(line)
45
+
46
+ if entry.type == 'Kup'
47
+ transactions << Transaction.new(
48
+ exchange: 'Bitcurex',
49
+ bought_currency: bitcoin,
50
+ sold_currency: entry.market,
51
+ time: entry.date,
52
+ bought_amount: entry.amount,
53
+ sold_amount: entry.total
54
+ )
55
+ elsif entry.type == 'Sprzedaj'
56
+ transactions << Transaction.new(
57
+ exchange: 'Bitcurex',
58
+ bought_currency: entry.market,
59
+ sold_currency: bitcoin,
60
+ time: entry.date,
61
+ bought_amount: entry.total,
62
+ sold_amount: entry.amount
63
+ )
64
+ else
65
+ raise "Bitcurex importer error: unexpected entry type '#{entry.type}'"
66
+ end
67
+ end
68
+
69
+ transactions.reverse
70
+ end
71
+ end
72
+ end
73
+ end
@@ -58,7 +58,7 @@ module CoinSync
58
58
  transactions << Transaction.new(
59
59
  exchange: 'Bittrex',
60
60
  time: entry.time_closed,
61
- bought_amount: entry.price - entry.commission, # TODO check this
61
+ bought_amount: entry.price - entry.commission,
62
62
  bought_currency: entry.currency,
63
63
  sold_amount: entry.quantity,
64
64
  sold_currency: entry.asset
@@ -68,7 +68,10 @@ module CoinSync
68
68
  entry.lp = line[0].to_i
69
69
  entry.exchange = line[1]
70
70
  entry.type = line[2]
71
- entry.date = Time.parse(line[3])
71
+
72
+ time = Time.parse(line[3])
73
+ entry.date = @config.timezone ? @config.timezone.local_to_utc(time) : time
74
+
72
75
  entry.amount = @formatter.parse_decimal(line[4])
73
76
  entry.asset = CryptoCurrency.new(line[5])
74
77
  entry.total = @formatter.parse_decimal(line[6])
@@ -43,7 +43,8 @@ module CoinSync
43
43
  end
44
44
 
45
45
  def import_transactions(filename)
46
- response = make_request('/order/dealt', limit: 100) # TODO: what if there's more than 100?
46
+ # TODO: what if there's more than 100? (looks like we might need to switch to loading each market separately…)
47
+ response = make_request('/order/dealt', limit: 100)
47
48
 
48
49
  case response
49
50
  when Net::HTTPSuccess
@@ -128,8 +129,16 @@ module CoinSync
128
129
  sold_amount: entry.deal_value,
129
130
  sold_currency: entry.coin_type_pair
130
131
  )
132
+ elsif entry.direction == 'SELL'
133
+ transactions << Transaction.new(
134
+ exchange: 'Kucoin',
135
+ time: entry.created_at,
136
+ sold_amount: entry.amount,
137
+ sold_currency: entry.coin_type,
138
+ bought_amount: entry.deal_value - entry.fee,
139
+ bought_currency: entry.coin_type_pair
140
+ )
131
141
  else
132
- # TODO sell
133
142
  raise "Kucoin importer error: unexpected entry direction '#{entry.direction}'"
134
143
  end
135
144
  end
@@ -16,7 +16,7 @@ module CoinSync
16
16
  csv << headers
17
17
 
18
18
  transactions.each do |tx|
19
- csv << transaction_to_csv(tx)
19
+ process_transaction(tx, csv)
20
20
  end
21
21
  end
22
22
  end
@@ -45,6 +45,10 @@ module CoinSync
45
45
  line
46
46
  end
47
47
 
48
+ def process_transaction(tx, csv)
49
+ csv << transaction_to_csv(tx)
50
+ end
51
+
48
52
  def transaction_to_csv(tx)
49
53
  if tx.purchase? || tx.sale?
50
54
  fiat_transaction_to_csv(tx)
@@ -1,3 +1,3 @@
1
1
  module CoinSync
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
metadata CHANGED
@@ -1,15 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coinsync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kuba Suder
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-24 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2018-04-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: tzinfo
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.2.5
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 1.2.5
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
13
33
  description:
14
34
  email:
15
35
  - jakub.suder@gmail.com
@@ -45,6 +65,7 @@ files:
45
65
  - lib/coinsync/importers/bitbay20.rb
46
66
  - lib/coinsync/importers/bitbay_api.rb
47
67
  - lib/coinsync/importers/bitcurex.rb
68
+ - lib/coinsync/importers/bitstamp_csv.rb
48
69
  - lib/coinsync/importers/bittrex_api.rb
49
70
  - lib/coinsync/importers/bittrex_csv.rb
50
71
  - lib/coinsync/importers/changelly.rb