coinsync 0.2.0 → 0.2.1

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