cointools 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +39 -10
- data/bin/cmcap +26 -22
- data/bin/coincap +26 -24
- data/bin/cryptowatch +34 -22
- data/lib/cointools/base_struct.rb +21 -0
- data/lib/cointools/coincap.rb +41 -45
- data/lib/cointools/coinmarketcap.rb +172 -67
- data/lib/cointools/cryptowatch.rb +106 -67
- data/lib/cointools/errors.rb +57 -0
- data/lib/cointools/request.rb +21 -0
- data/lib/cointools/utils.rb +20 -0
- data/lib/cointools/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 652a3663b1f2a32c461ddf19bb73f95cdadf155c
|
4
|
+
data.tar.gz: 9cd0d5c8891c59fcfebd24dfd038da884a8495f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 36720bdfcb1f20d8727684fadd4f61cb28e5cb574eb59cb60db1f2ac8d9017f0b3010db97f3e601d4fd027de0a512f4cca4819125b1dfa3f2066ad717f35c56f
|
7
|
+
data.tar.gz: af00dbb857cbc28ff07bfc84895e044d0f2cfcebef8b2c9e083ef1d58014567a1c47aa29eb7eef70c2c5d7b1f7f4e5ce5c252b4e4381eab9c4794d29b0d0f420
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
#### Version 0.5.0 (12.07.2018)
|
2
|
+
|
3
|
+
* switched to CoinMarketCap API v2, CMC's `get_price` and `get_price_by_symbol` calls now return data like coin name/symbol, rank and market cap
|
4
|
+
* new `get_all_prices` method in CMC that returns all 1600+ coins sorted by rank
|
5
|
+
* renamed `--list-exchanges` to `--exchanges`, `--list-markets` to `--markets`, `--list-fiat-currencies` to `--fiat-currencies`, `--fiat-currency` to `--convert-to`
|
6
|
+
* all commands print verbose output by default, use `--quiet/-q` to print only the price
|
7
|
+
* time can be passed as a string to `get_price` methods
|
8
|
+
* improved error handling
|
9
|
+
* added a huge amount of tests
|
10
|
+
|
1
11
|
#### Version 0.4.0 (23.05.2018)
|
2
12
|
|
3
13
|
* added CoinCap.io class and `coincap` command
|
data/README.md
CHANGED
@@ -37,11 +37,13 @@ To check the current price, skip the timestamp:
|
|
37
37
|
cryptowatch bitfinex btcusd
|
38
38
|
```
|
39
39
|
|
40
|
+
By default the price is printed in a verbose format that includes the requested market name and the time of the returned price. Add a `-q/--quiet` option to only print the price as a single number (e.g. to pass it further to another script).
|
41
|
+
|
40
42
|
You can fetch a list of available exchanges and markets using these commands:
|
41
43
|
|
42
44
|
```
|
43
|
-
cryptowatch --
|
44
|
-
cryptowatch --
|
45
|
+
cryptowatch --exchanges
|
46
|
+
cryptowatch --markets bithumb
|
45
47
|
```
|
46
48
|
|
47
49
|
In code:
|
@@ -85,7 +87,9 @@ Alternatively, you can pass the cryptocurrency's symbol using the `-s` or `--sym
|
|
85
87
|
cmcap -s powr
|
86
88
|
```
|
87
89
|
|
88
|
-
However,
|
90
|
+
However, be aware that coin symbols are not unique on CoinMarketCap - there are at least a few examples of duplicates. In such case, the first coin that matches (probably the older one) will be returned.
|
91
|
+
|
92
|
+
By default the price is printed in a verbose format that includes the requested coin symbol and the time of the returned price. Add a `-q/--quiet` option to only print the price as a single number (e.g. to pass it further to another script).
|
89
93
|
|
90
94
|
You can also use the `-b` or `--btc-price` flag to request a price in BTC instead of USD:
|
91
95
|
|
@@ -93,13 +97,13 @@ You can also use the `-b` or `--btc-price` flag to request a price in BTC instea
|
|
93
97
|
cmcap power-ledger -b
|
94
98
|
```
|
95
99
|
|
96
|
-
Or you can request the price in one of the ~30 other supported fiat currencies with `-
|
100
|
+
Or you can request the price in one of the ~30 other supported fiat currencies with `-c` or `--convert-to`:
|
97
101
|
|
98
102
|
```
|
99
|
-
cmcap request-network -
|
103
|
+
cmcap request-network -cEUR
|
100
104
|
```
|
101
105
|
|
102
|
-
You can print a list of supported fiat currencies with `cmcap --
|
106
|
+
You can print a list of supported fiat currencies with `cmcap --fiat-currencies`.
|
103
107
|
|
104
108
|
Same things in code:
|
105
109
|
|
@@ -109,17 +113,40 @@ cmc = CoinTools::CoinMarketCap.new
|
|
109
113
|
|
110
114
|
p CoinTools::CoinMarketCap::FIAT_CURRENCIES
|
111
115
|
|
112
|
-
ltc =
|
116
|
+
ltc = cmc.get_price('litecoin')
|
113
117
|
puts "LTC: #{ltc.usd_price} USD / #{ltc.btc_price} BTC"
|
114
118
|
|
115
|
-
xmr =
|
119
|
+
xmr = cmc.get_price_by_symbol('xmr')
|
116
120
|
puts "XMR: #{xmr.usd_price} USD / #{xmr.btc_price} BTC"
|
117
121
|
|
118
|
-
eth =
|
122
|
+
eth = cmc.get_price('ethereum', convert_to: 'EUR')
|
119
123
|
puts "ETH: #{eth.converted_price} EUR"
|
120
124
|
```
|
121
125
|
|
122
|
-
The soft rate limit for the API is
|
126
|
+
The soft rate limit for the API is 30 requests per minute (for API v2).
|
127
|
+
|
128
|
+
Note: since in the v2 API specific coin tickers can only be looked up using CoinMarketCap's internal numeric ids (e.g. Ethereum = 1027), both lookup methods available here - by the coin name ("slug") and symbol - have to first download a `/listings` JSON with a mapping of all coins on the site. The result of that call is cached in a `CoinTools::CoinMarketCap` object, so if you do many lookups in code, in one go or over some period of time, it's recommended to reuse the object instead of recreating it each time.
|
129
|
+
|
130
|
+
The API only allows one additional currency apart from USD, so if you pass a `convert_to` parameter, `btc_price` will not be returned, only `converted_price`.
|
131
|
+
|
132
|
+
|
133
|
+
### Getting all coin prices
|
134
|
+
|
135
|
+
You can also download the whole table of 1600+ coins listed on CoinMarketCap using the `get_all_prices` method. The method also takes a `convert_to` parameter, which works as described above. This method has to use paging to download data in batches and currently makes around 16 calls (which takes about half a minute), so do not call it repeatedly or you might go over the rate limit.
|
136
|
+
|
137
|
+
You can pass a block to this method to receive each batch of coins as they arrive. Take into account however that coins are not sorted by rank, but by numeric id (or roughly by creation date), so if you want e.g. top 100 coins, you still need to download the whole list. This is mostly intended to let you track progress in some way, e.g.:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
require 'cointools/coinmarketcap'
|
141
|
+
cmc = CoinTools::CoinMarketCap.new
|
142
|
+
|
143
|
+
coins = cmc.get_all_prices { |l| print '.' }
|
144
|
+
puts
|
145
|
+
|
146
|
+
coins.first(50).each do |c|
|
147
|
+
puts c.symbol.ljust(5) + c.market_cap.to_i.to_s.rjust(15)
|
148
|
+
end
|
149
|
+
```
|
123
150
|
|
124
151
|
|
125
152
|
## CoinCap
|
@@ -138,6 +165,8 @@ To check the current price, skip the timestamp:
|
|
138
165
|
coincap xmr
|
139
166
|
```
|
140
167
|
|
168
|
+
By default the price is printed in a verbose format that includes the requested coin symbol and the time of the returned price. Add a `-q/--quiet` option to only print the price as a single number (e.g. to pass it further to another script).
|
169
|
+
|
141
170
|
You can also use the `-b` or `--btc-price` flag to request a price in BTC instead of USD, or `-e` or `--eur-price` for EUR:
|
142
171
|
|
143
172
|
```
|
data/bin/cmcap
CHANGED
@@ -1,37 +1,37 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
|
-
require 'cointools'
|
4
|
+
require 'cointools/coinmarketcap'
|
5
5
|
|
6
6
|
require 'optparse'
|
7
7
|
require 'time'
|
8
8
|
|
9
9
|
def print_help
|
10
|
-
puts "Usage: #{$PROGRAM_NAME} <coin_name> [-b/--btc-price] [-
|
10
|
+
puts "Usage: #{$PROGRAM_NAME} <coin_name> [-b/--btc-price] [-cXXX/--convert-to XXX] [-q/--quiet]"
|
11
11
|
puts " e.g.: #{$PROGRAM_NAME} litecoin"
|
12
12
|
puts
|
13
13
|
puts "* To look up the currency by symbol instead of name (slower, downloads more data):"
|
14
|
-
puts " #{$PROGRAM_NAME} -s/--symbol <symbol> [-b/--btc-price] [-
|
14
|
+
puts " #{$PROGRAM_NAME} -s/--symbol <symbol> [-b/--btc-price] [-cXXX/--convert-to XXX] [-q/--quiet]"
|
15
15
|
puts " e.g.: #{$PROGRAM_NAME} -s LTC"
|
16
16
|
puts
|
17
17
|
puts "* To list supported fiat currencies:"
|
18
|
-
puts " #{$PROGRAM_NAME} --
|
18
|
+
puts " #{$PROGRAM_NAME} --fiat-currences"
|
19
19
|
puts
|
20
20
|
puts "* -b / --btc-price: returns the coin's price in BTC instead of USD"
|
21
|
-
puts "* -
|
21
|
+
puts "* -cEUR / --convert-to EUR: returns the price in a given fiat currency instead of USD"
|
22
22
|
end
|
23
23
|
|
24
|
-
|
24
|
+
quiet = false
|
25
25
|
btc_price = false
|
26
26
|
fiat_currency = nil
|
27
27
|
symbol = nil
|
28
28
|
|
29
29
|
OptionParser.new do |opts|
|
30
|
-
opts.on('-
|
30
|
+
opts.on('-q', '--quiet') { quiet = true }
|
31
31
|
opts.on('-b', '--btc-price') { btc_price = true }
|
32
|
-
opts.on('-
|
32
|
+
opts.on('-cXXX', '--convert-to XXX') { |f| fiat_currency = f }
|
33
33
|
|
34
|
-
opts.on('--
|
34
|
+
opts.on('--fiat-currencies') do
|
35
35
|
puts CoinTools::CoinMarketCap::FIAT_CURRENCIES
|
36
36
|
exit 0
|
37
37
|
end
|
@@ -56,7 +56,7 @@ unless symbol
|
|
56
56
|
end
|
57
57
|
|
58
58
|
if fiat_currency && btc_price
|
59
|
-
puts "#{$PROGRAM_NAME}: --btc-price and --
|
59
|
+
puts "#{$PROGRAM_NAME}: --btc-price and --convert-to options cannot be used together"
|
60
60
|
exit 1
|
61
61
|
end
|
62
62
|
|
@@ -78,22 +78,26 @@ begin
|
|
78
78
|
unit = 'USD'
|
79
79
|
end
|
80
80
|
|
81
|
-
if
|
82
|
-
puts "#{symbol&.upcase || coin_name} @ #{result.time || Time.now} ==> #{price} #{unit}"
|
83
|
-
puts
|
84
|
-
else
|
81
|
+
if quiet
|
85
82
|
puts price
|
83
|
+
else
|
84
|
+
puts "#{symbol&.upcase || coin_name} @ #{result.last_updated || Time.now} ==> #{price} #{unit}"
|
85
|
+
puts
|
86
86
|
end
|
87
|
-
rescue CoinTools::
|
88
|
-
$stderr.puts "Error:
|
87
|
+
rescue CoinTools::InvalidSymbolError => e
|
88
|
+
$stderr.puts "Error: Invalid coin name: '#{symbol}'"
|
89
89
|
exit 1
|
90
|
-
rescue CoinTools::
|
91
|
-
$stderr.puts "Error:
|
90
|
+
rescue CoinTools::InvalidFiatCurrencyError => e
|
91
|
+
$stderr.puts "Error: Unsupported fiat currency: '#{fiat_currency}'"
|
92
92
|
exit 1
|
93
|
-
rescue CoinTools::
|
94
|
-
|
93
|
+
rescue CoinTools::UnknownCoinError => e
|
94
|
+
if symbol
|
95
|
+
$stderr.puts "Error: No such coin: #{symbol.upcase}"
|
96
|
+
else
|
97
|
+
$stderr.puts "Error: No such coin: #{coin_name} (remember to use CoinMarketCap's IDs, or the -s option)"
|
98
|
+
end
|
95
99
|
exit 1
|
96
|
-
rescue CoinTools::
|
97
|
-
$stderr.puts "Error:
|
100
|
+
rescue CoinTools::ResponseError => e
|
101
|
+
$stderr.puts "Error: Something went wrong: #{e.nice_message}"
|
98
102
|
exit 1
|
99
103
|
end
|
data/bin/coincap
CHANGED
@@ -1,25 +1,24 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
|
-
require 'cointools'
|
4
|
+
require 'cointools/coincap'
|
5
5
|
|
6
6
|
require 'optparse'
|
7
|
-
require 'time'
|
8
7
|
|
9
8
|
def print_help
|
10
|
-
puts "Usage: #{$PROGRAM_NAME} <symbol> [<date>] [-b/--btc-price | -e/--eur-price] [-
|
9
|
+
puts "Usage: #{$PROGRAM_NAME} <symbol> [<date>] [-b/--btc-price | -e/--eur-price] [-q/--quiet]"
|
11
10
|
puts " e.g.: #{$PROGRAM_NAME} NANO \"2017-12-20 15:00\""
|
12
11
|
puts
|
13
12
|
puts "* -b / --btc-price: returns the coin's price in BTC instead of USD (current prices only)"
|
14
13
|
puts "* -e / --eur-price: returns the coin's price in EUR (current prices only)"
|
15
14
|
end
|
16
15
|
|
17
|
-
|
16
|
+
quiet = false
|
18
17
|
btc_price = false
|
19
18
|
eur_price = false
|
20
19
|
|
21
20
|
OptionParser.new do |opts|
|
22
|
-
opts.on('-
|
21
|
+
opts.on('-q', '--quiet') { quiet = true }
|
23
22
|
opts.on('-b', '--btc-price') { btc_price = true }
|
24
23
|
opts.on('-e', '--eur-price') { eur_price = true }
|
25
24
|
|
@@ -37,19 +36,19 @@ if ARGV.length < 1 || ARGV.length > 2
|
|
37
36
|
end
|
38
37
|
|
39
38
|
symbol = ARGV[0]
|
40
|
-
date =
|
39
|
+
date = ARGV[1]
|
41
40
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
41
|
+
begin
|
42
|
+
if eur_price && btc_price
|
43
|
+
puts "#{$PROGRAM_NAME}: --btc-price and --eur-price options cannot be used together"
|
44
|
+
exit 1
|
45
|
+
end
|
46
46
|
|
47
|
-
if date && (eur_price || btc_price)
|
48
|
-
|
49
|
-
|
50
|
-
end
|
47
|
+
if date && (eur_price || btc_price)
|
48
|
+
puts "#{$PROGRAM_NAME}: --btc-price and --eur-price options cannot be used for historical prices"
|
49
|
+
exit 1
|
50
|
+
end
|
51
51
|
|
52
|
-
begin
|
53
52
|
result = CoinTools::CoinCap.new.get_price(symbol, date)
|
54
53
|
|
55
54
|
if btc_price
|
@@ -63,19 +62,22 @@ begin
|
|
63
62
|
unit = 'USD'
|
64
63
|
end
|
65
64
|
|
66
|
-
if
|
65
|
+
if quiet
|
66
|
+
puts price
|
67
|
+
else
|
67
68
|
puts "#{symbol.upcase} @ #{result.time || Time.now} ==> #{price} #{unit}"
|
68
69
|
puts
|
69
|
-
else
|
70
|
-
puts price
|
71
70
|
end
|
72
|
-
rescue CoinTools::
|
73
|
-
$stderr.puts "Error:
|
71
|
+
rescue CoinTools::InvalidSymbolError => e
|
72
|
+
$stderr.puts "Error: Invalid coin name: '#{symbol}'"
|
73
|
+
exit 1
|
74
|
+
rescue CoinTools::InvalidDateError => e
|
75
|
+
$stderr.puts "Error: Invalid date format: '#{date}' (#{e})"
|
74
76
|
exit 1
|
75
|
-
rescue CoinTools::
|
76
|
-
$stderr.puts "Error:
|
77
|
+
rescue CoinTools::UnknownCoinError => e
|
78
|
+
$stderr.puts "Error: No such coin: #{symbol.upcase}"
|
77
79
|
exit 1
|
78
|
-
rescue CoinTools::
|
79
|
-
$stderr.puts "Error: #{e}"
|
80
|
+
rescue CoinTools::ResponseError => e
|
81
|
+
$stderr.puts "Error: Something went wrong: #{e.nice_message}"
|
80
82
|
exit 1
|
81
83
|
end
|
data/bin/cryptowatch
CHANGED
@@ -1,27 +1,26 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
|
-
require 'cointools'
|
4
|
+
require 'cointools/cryptowatch'
|
5
5
|
|
6
6
|
require 'optparse'
|
7
|
-
require 'time'
|
8
7
|
|
9
8
|
def print_help
|
10
|
-
puts "Usage: #{$PROGRAM_NAME} <exchange> <market> [<date>] [-
|
9
|
+
puts "Usage: #{$PROGRAM_NAME} <exchange> <market> [<date>] [-q/--quiet] [-f/--fast]"
|
11
10
|
puts " e.g.: #{$PROGRAM_NAME} gdax btcusd \"2017-06-30 15:27\""
|
12
11
|
puts
|
13
12
|
puts "* --fast: tries to use less API time allowance at the cost of worse reliability"
|
14
13
|
puts "* To print a list of available exchanges:"
|
15
|
-
puts " #{$PROGRAM_NAME} --
|
14
|
+
puts " #{$PROGRAM_NAME} --exchanges"
|
16
15
|
puts "* To print a list of available markets on an exchange:"
|
17
|
-
puts " #{$PROGRAM_NAME} --
|
16
|
+
puts " #{$PROGRAM_NAME} --markets kraken"
|
18
17
|
end
|
19
18
|
|
20
|
-
|
19
|
+
quiet = false
|
21
20
|
fast = false
|
22
21
|
|
23
22
|
OptionParser.new do |opts|
|
24
|
-
opts.on('-
|
23
|
+
opts.on('-q', '--quiet') { quiet = true }
|
25
24
|
|
26
25
|
opts.on('-h', '--help') do
|
27
26
|
print_help
|
@@ -30,17 +29,23 @@ OptionParser.new do |opts|
|
|
30
29
|
|
31
30
|
opts.on('-f', '--fast') { fast = true }
|
32
31
|
|
33
|
-
opts.on('--
|
32
|
+
opts.on('--exchanges') do
|
34
33
|
puts CoinTools::Cryptowatch.new.exchanges
|
35
34
|
exit 0
|
36
35
|
end
|
37
36
|
|
38
|
-
opts.on('--
|
37
|
+
opts.on('--markets EXCHANGE') do |exchange|
|
39
38
|
begin
|
40
39
|
puts CoinTools::Cryptowatch.new.get_markets(exchange)
|
41
40
|
exit 0
|
42
|
-
rescue CoinTools::
|
43
|
-
$stderr.puts "Error:
|
41
|
+
rescue CoinTools::InvalidExchangeError => e
|
42
|
+
$stderr.puts "Error: Invalid exchange name: '#{exchange}'"
|
43
|
+
exit 1
|
44
|
+
rescue CoinTools::UnknownExchangeError => e
|
45
|
+
$stderr.puts "Error: Unknown exchange: #{exchange}"
|
46
|
+
exit 1
|
47
|
+
rescue CoinTools::ResponseError => e
|
48
|
+
$stderr.puts "Error: Something went wrong: #{e.nice_message}"
|
44
49
|
exit 1
|
45
50
|
end
|
46
51
|
end
|
@@ -55,25 +60,32 @@ end
|
|
55
60
|
|
56
61
|
exchange = ARGV[0].downcase
|
57
62
|
market = ARGV[1].downcase
|
58
|
-
date =
|
63
|
+
date = ARGV[2]
|
64
|
+
currency = market[-3..-1].upcase
|
59
65
|
|
60
66
|
begin
|
61
67
|
method = fast ? :get_price_fast : :get_price
|
62
68
|
result = CoinTools::Cryptowatch.new.send(method, exchange, market, date)
|
63
69
|
|
64
|
-
if
|
65
|
-
puts "#{exchange}:#{market} @ #{result.time || Time.now} ==> #{result.price}"
|
66
|
-
puts
|
67
|
-
else
|
70
|
+
if quiet
|
68
71
|
puts result.price
|
72
|
+
else
|
73
|
+
puts "#{exchange}:#{market} @ #{result.time || Time.now} ==> #{result.price} #{currency}"
|
74
|
+
puts
|
69
75
|
end
|
70
|
-
rescue CoinTools::
|
71
|
-
$stderr.puts "Error: #{
|
76
|
+
rescue CoinTools::InvalidExchangeError => e
|
77
|
+
$stderr.puts "Error: Invalid exchange name: '#{exchange}'"
|
78
|
+
exit 1
|
79
|
+
rescue CoinTools::InvalidSymbolError => e
|
80
|
+
$stderr.puts "Error: Invalid market name: '#{market}'"
|
81
|
+
exit 1
|
82
|
+
rescue CoinTools::InvalidDateError => e
|
83
|
+
$stderr.puts "Error: Invalid date format: '#{date}' (#{e})"
|
72
84
|
exit 1
|
73
|
-
rescue CoinTools::
|
74
|
-
$stderr.puts "Error:
|
85
|
+
rescue CoinTools::UnknownCoinError => e
|
86
|
+
$stderr.puts "Error: Unknown exchange/market pair: #{exchange}/#{market}"
|
75
87
|
exit 1
|
76
|
-
rescue CoinTools::
|
77
|
-
$stderr.puts "Error: #{e}
|
88
|
+
rescue CoinTools::ResponseError => e
|
89
|
+
$stderr.puts "Error: Something went wrong: #{e.nice_message}"
|
78
90
|
exit 1
|
79
91
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module CoinTools
|
2
|
+
class BaseStruct
|
3
|
+
def self.make(*fields)
|
4
|
+
struct = Class.new(self)
|
5
|
+
|
6
|
+
fields.each do |f|
|
7
|
+
raise ArgumentError.new("Invalid field name: #{f}") unless f.to_s =~ /[a-z][a-z_]*/
|
8
|
+
end
|
9
|
+
|
10
|
+
struct.class_eval <<-CODE
|
11
|
+
def initialize(#{fields.map { |f| "#{f}:" }.join(', ')})
|
12
|
+
#{fields.map { |f| "@#{f} = #{f}" }.join("\n")}
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader #{fields.map { |f| ":#{f}" }.join(', ')}
|
16
|
+
CODE
|
17
|
+
|
18
|
+
struct
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/cointools/coincap.rb
CHANGED
@@ -1,38 +1,30 @@
|
|
1
|
-
require_relative '
|
1
|
+
require_relative 'base_struct'
|
2
|
+
require_relative 'errors'
|
3
|
+
require_relative 'request'
|
4
|
+
require_relative 'utils'
|
2
5
|
|
3
|
-
require 'json'
|
4
6
|
require 'net/http'
|
5
7
|
require 'uri'
|
6
8
|
|
7
9
|
module CoinTools
|
8
10
|
class CoinCap
|
9
11
|
BASE_URL = "https://coincap.io"
|
10
|
-
USER_AGENT = "cointools/#{CoinTools::VERSION}"
|
11
|
-
|
12
|
-
DataPoint = Struct.new(:time, :usd_price, :eur_price, :btc_price)
|
13
|
-
|
14
12
|
PERIODS = [1, 7, 30, 90, 180, 365]
|
15
13
|
|
16
|
-
|
17
|
-
attr_reader :response
|
14
|
+
DataPoint = BaseStruct.make(:time, :usd_price, :eur_price, :btc_price)
|
18
15
|
|
19
|
-
def initialize(response)
|
20
|
-
super("#{response.code} #{response.message}")
|
21
|
-
@response = response
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
class BadRequestException < InvalidResponseException
|
26
|
-
end
|
27
|
-
|
28
|
-
class InvalidDateException < StandardError
|
29
|
-
end
|
30
16
|
|
31
17
|
def get_price(symbol, time = nil)
|
32
|
-
|
18
|
+
raise InvalidSymbolError if symbol.to_s.empty?
|
19
|
+
|
20
|
+
if time.nil?
|
21
|
+
return get_current_price(symbol)
|
22
|
+
elsif time.is_a?(String)
|
23
|
+
time = Utils.parse_time(time)
|
24
|
+
end
|
33
25
|
|
34
|
-
(time <= Time.now) or raise
|
35
|
-
(time.year >= 2009) or raise
|
26
|
+
(time <= Time.now) or raise InvalidDateError.new('Future date was passed')
|
27
|
+
(time.year >= 2009) or raise InvalidDateError.new('Too early date was passed')
|
36
28
|
|
37
29
|
period = period_for_time(time)
|
38
30
|
|
@@ -42,59 +34,63 @@ module CoinTools
|
|
42
34
|
url = URI("#{BASE_URL}/history/#{symbol.upcase}")
|
43
35
|
end
|
44
36
|
|
45
|
-
|
46
|
-
|
47
|
-
response = make_request(url)
|
37
|
+
response = Request.get(url)
|
48
38
|
|
49
39
|
case response
|
50
40
|
when Net::HTTPSuccess
|
51
|
-
json =
|
41
|
+
json = Utils.parse_json(response.body)
|
42
|
+
raise UnknownCoinError.new(response) if json.nil? || json.empty?
|
43
|
+
raise JSONError.new(response) unless json.is_a?(Hash)
|
44
|
+
|
52
45
|
data = json['price']
|
46
|
+
raise JSONError.new(response) unless data.is_a?(Array)
|
53
47
|
|
48
|
+
unixtime = time.to_i
|
54
49
|
timestamp, price = best_matching_record(data, unixtime)
|
50
|
+
raise NoDataError.new(response) if timestamp.nil? || price.nil?
|
51
|
+
|
55
52
|
actual_time = Time.at(timestamp / 1000)
|
56
53
|
|
57
|
-
return DataPoint.new(actual_time, price, nil, nil)
|
58
|
-
when Net::
|
59
|
-
raise
|
54
|
+
return DataPoint.new(time: actual_time, usd_price: price, eur_price: nil, btc_price: nil)
|
55
|
+
when Net::HTTPClientError
|
56
|
+
raise BadRequestError.new(response)
|
60
57
|
else
|
61
|
-
raise
|
58
|
+
raise ServiceUnavailableError.new(response)
|
62
59
|
end
|
63
60
|
end
|
64
61
|
|
65
62
|
def get_current_price(symbol)
|
63
|
+
raise InvalidSymbolError if symbol.to_s.empty?
|
64
|
+
|
66
65
|
url = URI("#{BASE_URL}/page/#{symbol.upcase}")
|
67
66
|
|
68
|
-
response =
|
67
|
+
response = Request.get(url)
|
69
68
|
|
70
69
|
case response
|
71
70
|
when Net::HTTPSuccess
|
72
|
-
json =
|
71
|
+
json = Utils.parse_json(response.body)
|
72
|
+
raise UnknownCoinError.new(response) if json.nil? || json.empty?
|
73
|
+
raise JSONError.new(response) unless json.is_a?(Hash)
|
73
74
|
|
74
75
|
usd_price = json['price_usd']
|
75
76
|
eur_price = json['price_eur']
|
76
77
|
btc_price = json['price_btc']
|
77
78
|
|
78
|
-
|
79
|
-
|
80
|
-
|
79
|
+
if usd_price || eur_price || btc_price
|
80
|
+
return DataPoint.new(time: nil, usd_price: usd_price, eur_price: eur_price, btc_price: btc_price)
|
81
|
+
else
|
82
|
+
raise NoDataError.new(response)
|
83
|
+
end
|
84
|
+
when Net::HTTPClientError
|
85
|
+
raise BadRequestError.new(response)
|
81
86
|
else
|
82
|
-
raise
|
87
|
+
raise ServiceUnavailableError.new(response)
|
83
88
|
end
|
84
89
|
end
|
85
90
|
|
86
91
|
|
87
92
|
private
|
88
93
|
|
89
|
-
def make_request(url)
|
90
|
-
Net::HTTP.start(url.host, url.port, use_ssl: true) do |http|
|
91
|
-
request = Net::HTTP::Get.new(url)
|
92
|
-
request['User-Agent'] = USER_AGENT
|
93
|
-
|
94
|
-
http.request(request)
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
94
|
def best_matching_record(data, unixtime)
|
99
95
|
millitime = unixtime * 1000
|
100
96
|
data.sort_by { |record| (record[0] - millitime).abs }.first
|
@@ -1,13 +1,14 @@
|
|
1
|
-
require_relative '
|
1
|
+
require_relative 'base_struct'
|
2
|
+
require_relative 'errors'
|
3
|
+
require_relative 'request'
|
4
|
+
require_relative 'utils'
|
2
5
|
|
3
|
-
require 'json'
|
4
6
|
require 'net/http'
|
5
7
|
require 'uri'
|
6
8
|
|
7
9
|
module CoinTools
|
8
10
|
class CoinMarketCap
|
9
11
|
BASE_URL = "https://api.coinmarketcap.com"
|
10
|
-
USER_AGENT = "cointools/#{CoinTools::VERSION}"
|
11
12
|
|
12
13
|
FIAT_CURRENCIES = [
|
13
14
|
'AUD', 'BRL', 'CAD', 'CHF', 'CLP', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP',
|
@@ -16,120 +17,224 @@ module CoinTools
|
|
16
17
|
'ZAR'
|
17
18
|
]
|
18
19
|
|
19
|
-
class
|
20
|
-
attr_reader :
|
20
|
+
class Listing
|
21
|
+
attr_reader :numeric_id, :name, :symbol, :text_id
|
21
22
|
|
22
|
-
def initialize(
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
def initialize(json)
|
24
|
+
[:id, :name, :symbol, :website_slug].each do |key|
|
25
|
+
raise ArgumentError.new("Missing #{key} field") unless json[key.to_s]
|
26
|
+
end
|
27
|
+
|
28
|
+
@numeric_id = json['id']
|
29
|
+
@name = json['name']
|
30
|
+
@symbol = json['symbol']
|
31
|
+
@text_id = json['website_slug']
|
27
32
|
end
|
28
33
|
end
|
29
34
|
|
30
|
-
class
|
31
|
-
attr_reader :
|
35
|
+
class CoinData < Listing
|
36
|
+
attr_reader :last_updated, :usd_price, :btc_price, :converted_price, :rank, :market_cap
|
37
|
+
|
38
|
+
def initialize(json, convert_to = nil)
|
39
|
+
super(json)
|
40
|
+
|
41
|
+
raise ArgumentError.new('Missing rank field') unless json['rank']
|
42
|
+
raise ArgumentError.new('Invalid rank field') unless json['rank'].is_a?(Integer) && json['rank'] > 0
|
43
|
+
@rank = json['rank']
|
32
44
|
|
33
|
-
|
34
|
-
|
35
|
-
|
45
|
+
quotes = json['quotes']
|
46
|
+
raise ArgumentError.new('Missing quotes field') unless quotes
|
47
|
+
raise ArgumentError.new('Invalid quotes field') unless quotes.is_a?(Hash)
|
48
|
+
|
49
|
+
usd_quote = quotes['USD']
|
50
|
+
raise ArgumentError.new('Missing USD quote info') unless usd_quote
|
51
|
+
raise ArgumentError.new('Invalid USD quote info') unless usd_quote.is_a?(Hash)
|
52
|
+
|
53
|
+
@usd_price = usd_quote['price']&.to_f
|
54
|
+
@market_cap = usd_quote['market_cap']&.to_f
|
55
|
+
|
56
|
+
if convert_to
|
57
|
+
converted_quote = quotes[convert_to.upcase]
|
58
|
+
|
59
|
+
if converted_quote
|
60
|
+
raise ArgumentError.new("Invalid #{convert_to.upcase} quote info") unless converted_quote.is_a?(Hash)
|
61
|
+
@converted_price = converted_quote['price']&.to_f
|
62
|
+
end
|
63
|
+
else
|
64
|
+
btc_quote = quotes['BTC']
|
65
|
+
|
66
|
+
if btc_quote
|
67
|
+
raise ArgumentError.new("Invalid BTC quote info") unless btc_quote.is_a?(Hash)
|
68
|
+
@btc_price = btc_quote['price']&.to_f
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
timestamp = json['last_updated']
|
73
|
+
@last_updated = Time.at(timestamp) if timestamp
|
36
74
|
end
|
37
75
|
end
|
38
76
|
|
39
|
-
|
77
|
+
def symbol_map
|
78
|
+
load_listings if @symbol_map.nil?
|
79
|
+
@symbol_map
|
40
80
|
end
|
41
81
|
|
42
|
-
|
82
|
+
def id_map
|
83
|
+
load_listings if @id_map.nil?
|
84
|
+
@id_map
|
43
85
|
end
|
44
86
|
|
45
|
-
|
46
|
-
|
87
|
+
def load_listings
|
88
|
+
url = URI("#{BASE_URL}/v2/listings/")
|
89
|
+
|
90
|
+
response = Request.get(url)
|
91
|
+
|
92
|
+
case response
|
93
|
+
when Net::HTTPSuccess
|
94
|
+
data = parse_response(response, Array)
|
47
95
|
|
48
|
-
|
96
|
+
@id_map = {}
|
97
|
+
@symbol_map = {}
|
98
|
+
|
99
|
+
begin
|
100
|
+
data.each do |record|
|
101
|
+
listing = Listing.new(record)
|
102
|
+
|
103
|
+
@id_map[listing.text_id] = listing
|
104
|
+
@symbol_map[listing.symbol] = listing
|
105
|
+
end
|
106
|
+
rescue ArgumentError => e
|
107
|
+
# TODO: JSONError vs. NoDataError? + error class docs
|
108
|
+
raise JSONError.new(response, e.message)
|
109
|
+
end
|
110
|
+
|
111
|
+
data.length
|
112
|
+
when Net::HTTPClientError
|
113
|
+
raise BadRequestError.new(response)
|
114
|
+
else
|
115
|
+
raise ServiceUnavailableError.new(response)
|
116
|
+
end
|
49
117
|
end
|
50
118
|
|
51
119
|
def get_price(coin_name, convert_to: nil)
|
52
|
-
|
120
|
+
raise InvalidSymbolError if coin_name.to_s.empty?
|
121
|
+
|
122
|
+
validate_fiat_currency(convert_to) if convert_to
|
123
|
+
|
124
|
+
listing = id_map[coin_name.to_s]
|
125
|
+
raise InvalidSymbolError if listing.nil?
|
126
|
+
|
127
|
+
get_price_for_listing(listing, convert_to: convert_to)
|
128
|
+
end
|
129
|
+
|
130
|
+
def get_price_by_symbol(coin_symbol, convert_to: nil)
|
131
|
+
raise InvalidSymbolError if coin_symbol.to_s.empty?
|
132
|
+
|
133
|
+
validate_fiat_currency(convert_to) if convert_to
|
134
|
+
|
135
|
+
listing = symbol_map[coin_symbol.to_s]
|
136
|
+
raise InvalidSymbolError if listing.nil?
|
137
|
+
|
138
|
+
get_price_for_listing(listing, convert_to: convert_to)
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_price_for_listing(listing, convert_to: nil)
|
142
|
+
url = URI("#{BASE_URL}/v2/ticker/#{listing.numeric_id}/")
|
53
143
|
|
54
144
|
if convert_to
|
55
|
-
|
56
|
-
|
145
|
+
url.query = "convert=#{convert_to.upcase}"
|
146
|
+
else
|
147
|
+
url.query = "convert=BTC"
|
57
148
|
end
|
58
149
|
|
59
|
-
response =
|
150
|
+
response = Request.get(url)
|
60
151
|
|
61
152
|
case response
|
62
153
|
when Net::HTTPSuccess
|
63
|
-
|
64
|
-
record = json[0]
|
65
|
-
|
66
|
-
usd_price = record['price_usd']&.to_f
|
67
|
-
btc_price = record['price_btc']&.to_f
|
68
|
-
timestamp = Time.at(record['last_updated'].to_i)
|
154
|
+
record = parse_response(response, Hash)
|
69
155
|
|
70
|
-
|
71
|
-
|
72
|
-
|
156
|
+
begin
|
157
|
+
return CoinData.new(record, convert_to)
|
158
|
+
rescue ArgumentError => e
|
159
|
+
raise JSONError.new(response, e.message)
|
73
160
|
end
|
74
|
-
|
75
|
-
return DataPoint.new(timestamp, usd_price, btc_price, converted_price)
|
76
161
|
when Net::HTTPNotFound
|
77
|
-
raise
|
162
|
+
raise UnknownCoinError.new(response)
|
163
|
+
when Net::HTTPClientError
|
164
|
+
raise BadRequestError.new(response)
|
78
165
|
else
|
79
|
-
raise
|
166
|
+
raise ServiceUnavailableError.new(response)
|
80
167
|
end
|
81
168
|
end
|
82
169
|
|
83
|
-
def
|
84
|
-
url = URI("#{BASE_URL}/
|
85
|
-
symbol = coin_symbol.downcase
|
170
|
+
def get_all_prices(convert_to: nil, &block)
|
171
|
+
url = URI("#{BASE_URL}/v2/ticker/?structure=array&sort=id&limit=100")
|
86
172
|
|
87
173
|
if convert_to
|
88
174
|
validate_fiat_currency(convert_to)
|
89
|
-
url.query += "&convert=#{convert_to}"
|
175
|
+
url.query += "&convert=#{convert_to.upcase}"
|
176
|
+
else
|
177
|
+
url.query += "&convert=BTC"
|
90
178
|
end
|
91
179
|
|
92
|
-
|
180
|
+
start = 0
|
181
|
+
coins = []
|
182
|
+
|
183
|
+
loop do
|
184
|
+
new_batch = fetch_full_ticker_page(url, convert_to, coins.length, &block)
|
185
|
+
|
186
|
+
if new_batch.empty?
|
187
|
+
break
|
188
|
+
else
|
189
|
+
coins.concat(new_batch)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
coins.sort_by(&:rank)
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def fetch_full_ticker_page(base_url, convert_to, start)
|
200
|
+
url = base_url.clone
|
201
|
+
url.query += "&start=#{start}"
|
202
|
+
response = Request.get(url)
|
93
203
|
|
94
204
|
case response
|
95
205
|
when Net::HTTPSuccess
|
96
|
-
|
97
|
-
record = json.detect { |r| r['symbol'].downcase == symbol }
|
98
|
-
raise NoDataException.new('No coin found with given symbol') if record.nil?
|
99
|
-
|
100
|
-
usd_price = record['price_usd']&.to_f
|
101
|
-
btc_price = record['price_btc']&.to_f
|
102
|
-
timestamp = Time.at(record['last_updated'].to_i)
|
206
|
+
data = parse_response(response, Array)
|
103
207
|
|
104
|
-
|
105
|
-
|
106
|
-
|
208
|
+
begin
|
209
|
+
new_batch = data.map { |record| CoinData.new(record, convert_to) }
|
210
|
+
rescue ArgumentError => e
|
211
|
+
raise JSONError.new(response, e.message)
|
107
212
|
end
|
108
213
|
|
109
|
-
|
214
|
+
yield new_batch if block_given? && !new_batch.empty?
|
215
|
+
|
216
|
+
new_batch
|
110
217
|
when Net::HTTPNotFound
|
111
|
-
|
218
|
+
[]
|
219
|
+
when Net::HTTPClientError
|
220
|
+
raise BadRequestError.new(response)
|
112
221
|
else
|
113
|
-
raise
|
222
|
+
raise ServiceUnavailableError.new(response)
|
114
223
|
end
|
115
224
|
end
|
116
225
|
|
117
|
-
|
118
|
-
private
|
119
|
-
|
120
226
|
def validate_fiat_currency(fiat_currency)
|
121
|
-
unless FIAT_CURRENCIES.include?(fiat_currency.upcase)
|
122
|
-
raise InvalidFiatCurrencyException
|
123
|
-
end
|
227
|
+
raise InvalidFiatCurrencyError unless FIAT_CURRENCIES.include?(fiat_currency.upcase)
|
124
228
|
end
|
125
229
|
|
126
|
-
def
|
127
|
-
|
128
|
-
request = Net::HTTP::Get.new(url)
|
129
|
-
request['User-Agent'] = USER_AGENT
|
230
|
+
def parse_response(response, expected_data_type)
|
231
|
+
json = Utils.parse_json(response.body)
|
130
232
|
|
131
|
-
|
132
|
-
|
233
|
+
raise JSONError.new(response) unless json.is_a?(Hash) && json['metadata']
|
234
|
+
raise BadRequestError.new(response, json['metadata']['error']) if json['metadata']['error']
|
235
|
+
raise JSONError.new(response) unless json['data'].is_a?(expected_data_type)
|
236
|
+
|
237
|
+
json['data']
|
133
238
|
end
|
134
239
|
end
|
135
240
|
end
|
@@ -1,15 +1,14 @@
|
|
1
|
-
require_relative '
|
1
|
+
require_relative 'base_struct'
|
2
|
+
require_relative 'errors'
|
3
|
+
require_relative 'request'
|
4
|
+
require_relative 'utils'
|
2
5
|
|
3
|
-
require 'json'
|
4
6
|
require 'net/http'
|
5
7
|
require 'uri'
|
6
8
|
|
7
9
|
module CoinTools
|
8
10
|
class Cryptowatch
|
9
11
|
BASE_URL = "https://api.cryptowat.ch"
|
10
|
-
USER_AGENT = "cointools/#{CoinTools::VERSION}"
|
11
|
-
|
12
|
-
DataPoint = Struct.new(:price, :time, :api_time_spent, :api_time_remaining)
|
13
12
|
|
14
13
|
# we expect this many days worth of data for a given period precision (in seconds); NOT guaranteed by the API
|
15
14
|
DAYS_FOR_PERIODS = {
|
@@ -17,96 +16,130 @@ module CoinTools
|
|
17
16
|
7200 => 365, 14400 => 1.5 * 365, 21600 => 2 * 365, 43200 => 3 * 365, 86400 => 4 * 365
|
18
17
|
}
|
19
18
|
|
20
|
-
|
21
|
-
attr_reader :response
|
22
|
-
|
23
|
-
def initialize(response)
|
24
|
-
super("#{response.code} #{response.message}")
|
25
|
-
@response = response
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
class BadRequestException < InvalidResponseException
|
30
|
-
end
|
19
|
+
DataPoint = BaseStruct.make(:price, :time, :api_time_spent, :api_time_remaining)
|
31
20
|
|
32
|
-
class NoDataException < StandardError
|
33
|
-
end
|
34
|
-
|
35
|
-
class InvalidDateException < StandardError
|
36
|
-
end
|
37
21
|
|
38
22
|
def exchanges
|
39
23
|
@exchanges ||= get_exchanges
|
40
24
|
end
|
41
25
|
|
42
26
|
def get_markets(exchange)
|
27
|
+
raise InvalidExchangeError if exchange.to_s.empty?
|
28
|
+
|
43
29
|
url = URI("#{BASE_URL}/markets/#{exchange}")
|
44
30
|
|
45
|
-
response =
|
31
|
+
response = Request.get(url)
|
46
32
|
|
47
33
|
case response
|
48
34
|
when Net::HTTPSuccess
|
49
|
-
json =
|
35
|
+
json = Utils.parse_json(response.body)
|
36
|
+
raise JSONError.new(response) unless json.is_a?(Hash) && json['result'].is_a?(Array)
|
37
|
+
|
50
38
|
return json['result'].select { |m| m['active'] == true }.map { |m| m['pair'] }.sort
|
51
|
-
when Net::
|
52
|
-
raise
|
39
|
+
when Net::HTTPNotFound
|
40
|
+
raise UnknownExchangeError.new(response)
|
41
|
+
when Net::HTTPClientError
|
42
|
+
raise BadRequestError.new(response)
|
53
43
|
else
|
54
|
-
raise
|
44
|
+
raise ServiceUnavailableError.new(response)
|
55
45
|
end
|
56
46
|
end
|
57
47
|
|
58
48
|
def get_price(exchange, market, time = nil)
|
59
|
-
|
49
|
+
raise InvalidExchangeError if exchange.to_s.empty?
|
50
|
+
raise InvalidSymbolError if market.to_s.empty?
|
60
51
|
|
61
|
-
|
62
|
-
|
52
|
+
if time.nil?
|
53
|
+
return get_current_price(exchange, market)
|
54
|
+
elsif time.is_a?(String)
|
55
|
+
time = Utils.parse_time(time)
|
56
|
+
end
|
57
|
+
|
58
|
+
(time <= Time.now) or raise InvalidDateError.new('Future date was passed')
|
59
|
+
(time.year >= 2009) or raise InvalidDateError.new('Too early date was passed')
|
63
60
|
|
64
61
|
unixtime = time.to_i
|
65
62
|
current_time = Time.now.to_i
|
66
63
|
url = URI("#{BASE_URL}/markets/#{exchange}/#{market}/ohlc?after=#{unixtime}")
|
67
64
|
|
68
|
-
response =
|
65
|
+
response = Request.get(url)
|
69
66
|
|
70
67
|
case response
|
71
68
|
when Net::HTTPSuccess
|
72
|
-
json =
|
69
|
+
json = Utils.parse_json(response.body)
|
70
|
+
raise JSONError.new(response) unless json.is_a?(Hash)
|
71
|
+
|
73
72
|
data = json['result']
|
74
73
|
allowance = json['allowance']
|
74
|
+
raise JSONError.new(response) unless data.is_a?(Hash) && allowance.is_a?(Hash)
|
75
75
|
|
76
76
|
timestamp, o, h, l, c, volume = best_matching_record(data, unixtime, current_time)
|
77
|
-
raise
|
77
|
+
raise NoDataError.new(response, 'No price data returned') unless timestamp && o
|
78
78
|
|
79
79
|
actual_time = Time.at(timestamp)
|
80
|
-
|
81
|
-
|
82
|
-
|
80
|
+
|
81
|
+
return DataPoint.new(
|
82
|
+
price: o,
|
83
|
+
time: actual_time,
|
84
|
+
api_time_spent: allowance['cost'],
|
85
|
+
api_time_remaining: allowance['remaining']
|
86
|
+
)
|
87
|
+
when Net::HTTPNotFound
|
88
|
+
raise UnknownCoinError.new(response)
|
89
|
+
when Net::HTTPClientError
|
90
|
+
raise BadRequestError.new(response)
|
83
91
|
else
|
84
|
-
raise
|
92
|
+
raise ServiceUnavailableError.new(response)
|
85
93
|
end
|
86
94
|
end
|
87
95
|
|
88
96
|
def get_current_price(exchange, market)
|
97
|
+
raise InvalidExchangeError if exchange.to_s.empty?
|
98
|
+
raise InvalidSymbolError if market.to_s.empty?
|
99
|
+
|
89
100
|
url = URI("#{BASE_URL}/markets/#{exchange}/#{market}/price")
|
90
101
|
|
91
|
-
response =
|
102
|
+
response = Request.get(url)
|
92
103
|
|
93
104
|
case response
|
94
105
|
when Net::HTTPSuccess
|
95
|
-
json =
|
96
|
-
|
97
|
-
allowance = json['allowance']
|
106
|
+
json = Utils.parse_json(response.body)
|
107
|
+
raise JSONError.new(response) unless json.is_a?(Hash)
|
98
108
|
|
99
|
-
|
100
|
-
|
101
|
-
raise
|
109
|
+
data = json['result']
|
110
|
+
allowance = json['allowance']
|
111
|
+
raise JSONError.new(response) unless data.is_a?(Hash) && allowance.is_a?(Hash)
|
112
|
+
|
113
|
+
price = data['price']
|
114
|
+
raise NoDataError.new(response) unless price
|
115
|
+
|
116
|
+
return DataPoint.new(
|
117
|
+
price: price,
|
118
|
+
time: nil,
|
119
|
+
api_time_spent: allowance['cost'],
|
120
|
+
api_time_remaining: allowance['remaining']
|
121
|
+
)
|
122
|
+
when Net::HTTPNotFound
|
123
|
+
raise UnknownCoinError.new(response)
|
124
|
+
when Net::HTTPClientError
|
125
|
+
raise BadRequestError.new(response)
|
102
126
|
else
|
103
|
-
raise
|
127
|
+
raise ServiceUnavailableError.new(response)
|
104
128
|
end
|
105
129
|
end
|
106
130
|
|
107
|
-
def get_price_fast(exchange, market, time)
|
108
|
-
|
109
|
-
|
131
|
+
def get_price_fast(exchange, market, time = nil)
|
132
|
+
raise InvalidExchangeError if exchange.to_s.empty?
|
133
|
+
raise InvalidSymbolError if market.to_s.empty?
|
134
|
+
|
135
|
+
if time.nil?
|
136
|
+
return get_current_price(exchange, market)
|
137
|
+
elsif time.is_a?(String)
|
138
|
+
time = Utils.parse_time(time)
|
139
|
+
end
|
140
|
+
|
141
|
+
(time <= Time.now) or raise InvalidDateError.new('Future date was passed')
|
142
|
+
(time.year >= 2009) or raise InvalidDateError.new('Too early date was passed')
|
110
143
|
|
111
144
|
period = precision_for_time(time)
|
112
145
|
|
@@ -118,43 +151,45 @@ module CoinTools
|
|
118
151
|
current_time = Time.now.to_i
|
119
152
|
url = URI("#{BASE_URL}/markets/#{exchange}/#{market}/ohlc?after=#{unixtime}&periods=#{period}")
|
120
153
|
|
121
|
-
response =
|
154
|
+
response = Request.get(url)
|
122
155
|
|
123
156
|
case response
|
124
157
|
when Net::HTTPSuccess
|
125
|
-
json =
|
158
|
+
json = Utils.parse_json(response.body)
|
159
|
+
raise JSONError.new(response) unless json.is_a?(Hash)
|
160
|
+
|
126
161
|
data = json['result']
|
127
162
|
allowance = json['allowance']
|
163
|
+
raise JSONError.new(response) unless data.is_a?(Hash) && allowance.is_a?(Hash)
|
128
164
|
|
129
165
|
timestamp, o, h, l, c, volume = best_matching_record(data, unixtime, current_time)
|
130
|
-
raise
|
166
|
+
raise NoDataError.new(response, 'No price data returned') unless timestamp && o
|
131
167
|
|
132
168
|
actual_time = Time.at(timestamp)
|
133
|
-
|
134
|
-
|
135
|
-
|
169
|
+
|
170
|
+
return DataPoint.new(
|
171
|
+
price: o,
|
172
|
+
time: actual_time,
|
173
|
+
api_time_spent: allowance['cost'],
|
174
|
+
api_time_remaining: allowance['remaining']
|
175
|
+
)
|
176
|
+
when Net::HTTPNotFound
|
177
|
+
raise UnknownCoinError.new(response)
|
178
|
+
when Net::HTTPClientError
|
179
|
+
raise BadRequestError.new(response)
|
136
180
|
else
|
137
|
-
raise
|
181
|
+
raise ServiceUnavailableError.new(response)
|
138
182
|
end
|
139
183
|
end
|
140
184
|
|
141
185
|
|
142
186
|
private
|
143
187
|
|
144
|
-
def make_request(url)
|
145
|
-
Net::HTTP.start(url.host, url.port, use_ssl: true) do |http|
|
146
|
-
request = Net::HTTP::Get.new(url)
|
147
|
-
request['User-Agent'] = USER_AGENT
|
148
|
-
|
149
|
-
http.request(request)
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
188
|
def best_matching_record(data, unixtime, request_time)
|
154
189
|
candidates = []
|
155
190
|
|
156
191
|
data.keys.sort_by { |k| k.to_i }.each do |k|
|
157
|
-
records = data[k]
|
192
|
+
records = data[k] || []
|
158
193
|
previous = nil
|
159
194
|
|
160
195
|
records.each do |record|
|
@@ -192,14 +227,18 @@ module CoinTools
|
|
192
227
|
def get_exchanges
|
193
228
|
url = URI("#{BASE_URL}/exchanges")
|
194
229
|
|
195
|
-
response =
|
230
|
+
response = Request.get(url)
|
196
231
|
|
197
232
|
case response
|
198
233
|
when Net::HTTPSuccess
|
199
|
-
json =
|
234
|
+
json = Utils.parse_json(response.body)
|
235
|
+
raise JSONError.new(response) unless json.is_a?(Hash) && json['result'].is_a?(Array)
|
236
|
+
|
200
237
|
return json['result'].select { |e| e['active'] == true }.map { |e| e['symbol'] }.sort
|
238
|
+
when Net::HTTPClientError
|
239
|
+
raise BadRequestError.new(response)
|
201
240
|
else
|
202
|
-
raise
|
241
|
+
raise ServiceUnavailableError.new(response)
|
203
242
|
end
|
204
243
|
end
|
205
244
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module CoinTools
|
2
|
+
class Error < StandardError
|
3
|
+
def nice_message
|
4
|
+
"#{self.class}: #{message}"
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
class UserError < Error
|
10
|
+
end
|
11
|
+
|
12
|
+
class InvalidDateError < UserError
|
13
|
+
end
|
14
|
+
|
15
|
+
class InvalidExchangeError < UserError
|
16
|
+
end
|
17
|
+
|
18
|
+
class InvalidFiatCurrencyError < UserError
|
19
|
+
end
|
20
|
+
|
21
|
+
class InvalidSymbolError < UserError
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
class ResponseError < Error
|
26
|
+
attr_reader :response
|
27
|
+
|
28
|
+
def initialize(response, message = nil)
|
29
|
+
super(message || "#{response.code} #{response.message}")
|
30
|
+
@response = response
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class JSONError < ResponseError
|
35
|
+
def initialize(response, message = nil)
|
36
|
+
super(response, message || "Incorrect JSON structure")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class NoDataError < ResponseError
|
41
|
+
def initialize(response, message = nil)
|
42
|
+
super(response, message || "Missing data in the response")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class BadRequestError < ResponseError
|
47
|
+
end
|
48
|
+
|
49
|
+
class UnknownCoinError < BadRequestError
|
50
|
+
end
|
51
|
+
|
52
|
+
class UnknownExchangeError < BadRequestError
|
53
|
+
end
|
54
|
+
|
55
|
+
class ServiceUnavailableError < ResponseError
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
require_relative 'version'
|
5
|
+
|
6
|
+
module CoinTools
|
7
|
+
module Request
|
8
|
+
USER_AGENT = "cointools/#{CoinTools::VERSION}"
|
9
|
+
|
10
|
+
def self.get(url)
|
11
|
+
url = URI(url) if url.is_a?(String)
|
12
|
+
|
13
|
+
Net::HTTP.start(url.host, url.port, use_ssl: true) do |http|
|
14
|
+
request = Net::HTTP::Get.new(url)
|
15
|
+
request['User-Agent'] = USER_AGENT
|
16
|
+
|
17
|
+
http.request(request)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'errors'
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
module CoinTools
|
7
|
+
module Utils
|
8
|
+
class << self
|
9
|
+
def parse_json(text)
|
10
|
+
JSON.parse(text, quirks_mode: true)
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse_time(text)
|
14
|
+
Time.parse(text)
|
15
|
+
rescue ArgumentError => e
|
16
|
+
raise InvalidDateError.new(e.message)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/cointools/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cointools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
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-
|
11
|
+
date: 2018-07-12 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -27,9 +27,13 @@ files:
|
|
27
27
|
- bin/coincap
|
28
28
|
- bin/cryptowatch
|
29
29
|
- lib/cointools.rb
|
30
|
+
- lib/cointools/base_struct.rb
|
30
31
|
- lib/cointools/coincap.rb
|
31
32
|
- lib/cointools/coinmarketcap.rb
|
32
33
|
- lib/cointools/cryptowatch.rb
|
34
|
+
- lib/cointools/errors.rb
|
35
|
+
- lib/cointools/request.rb
|
36
|
+
- lib/cointools/utils.rb
|
33
37
|
- lib/cointools/version.rb
|
34
38
|
homepage: https://github.com/mackuba/cointools
|
35
39
|
licenses:
|
@@ -51,7 +55,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
55
|
version: '0'
|
52
56
|
requirements: []
|
53
57
|
rubyforge_project:
|
54
|
-
rubygems_version: 2.5.2
|
58
|
+
rubygems_version: 2.5.2.3
|
55
59
|
signing_key:
|
56
60
|
specification_version: 4
|
57
61
|
summary: A collection of scripts for checking cryptocurrency prices.
|