honeymaker 0.11.5 → 0.11.6
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 +4 -4
- data/lib/honeymaker/exchanges/gemini.rb +28 -3
- data/lib/honeymaker/version.rb +1 -1
- data/test/honeymaker/exchanges/gemini_test.rb +83 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79df37d9e336e6aae8b38cbe4af40655742da763a47842b3b0128355130c2c48
|
|
4
|
+
data.tar.gz: 8a821ce866ad46433104a085375f5ac3073ac159c44e0b31f4f57170414a99fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea5bbaf9abffab526e1929b0c6e9d3ebc80af1e609fdb696a5c141585d57e643d99370acb1dcf458f6fe7c02df22c269b240e316ca68dfbd8295ef1b882ae4a4
|
|
7
|
+
data.tar.gz: 8dca53d930a98f7259f3c73d6e3827e40647fbf6ceaefd857c2795e1c499c630f335912c23a030a2a72aa326d578b1529d55bb825e2e54c08db56e794339da1f
|
|
@@ -5,13 +5,24 @@ module Honeymaker
|
|
|
5
5
|
class Gemini < Exchange
|
|
6
6
|
BASE_URL = "https://api.gemini.com"
|
|
7
7
|
|
|
8
|
+
# Gemini has no bulk symbol-details endpoint, so the catalogue is one request per symbol --
|
|
9
|
+
# ~350 of them against a public limit of 120/min. Unpaced they ran at ~208/min, and a single
|
|
10
|
+
# dropped connection failed the whole catalogue: the adapter retries nothing on its own.
|
|
11
|
+
REQUEST_INTERVAL = 0.5
|
|
12
|
+
REQUEST_ATTEMPTS = 3
|
|
13
|
+
TRANSIENT_ERRORS = [
|
|
14
|
+
Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::RequestTimeoutError,
|
|
15
|
+
Faraday::TooManyRequestsError, Faraday::ServerError
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
8
18
|
def get_tickers_info
|
|
9
19
|
with_rescue do
|
|
10
|
-
|
|
11
|
-
symbols = symbols_response.body
|
|
20
|
+
symbols = catalogue_get("/v1/symbols")
|
|
12
21
|
|
|
13
22
|
symbols.filter_map do |symbol|
|
|
14
|
-
|
|
23
|
+
# A symbol that still fails raises, failing the WHOLE catalogue. Never skip it: callers
|
|
24
|
+
# read a pair missing from the catalogue as delisted, and data-api revokes it.
|
|
25
|
+
detail = catalogue_get("/v1/symbols/details/#{symbol}")
|
|
15
26
|
|
|
16
27
|
tick_size = detail["tick_size"]&.to_s || "0.01"
|
|
17
28
|
quote_increment = detail["quote_increment"]&.to_s || "0.01"
|
|
@@ -50,6 +61,20 @@ module Honeymaker
|
|
|
50
61
|
def connection
|
|
51
62
|
@connection ||= build_connection(BASE_URL)
|
|
52
63
|
end
|
|
64
|
+
|
|
65
|
+
def catalogue_get(path)
|
|
66
|
+
attempt = 1
|
|
67
|
+
begin
|
|
68
|
+
sleep(REQUEST_INTERVAL)
|
|
69
|
+
connection.get(path).body
|
|
70
|
+
rescue *TRANSIENT_ERRORS
|
|
71
|
+
raise if attempt >= REQUEST_ATTEMPTS
|
|
72
|
+
|
|
73
|
+
sleep(2**attempt)
|
|
74
|
+
attempt += 1
|
|
75
|
+
retry
|
|
76
|
+
end
|
|
77
|
+
end
|
|
53
78
|
end
|
|
54
79
|
end
|
|
55
80
|
end
|
data/lib/honeymaker/version.rb
CHANGED
|
@@ -7,6 +7,9 @@ class Honeymaker::Exchanges::GeminiTest < Minitest::Test
|
|
|
7
7
|
|
|
8
8
|
def setup
|
|
9
9
|
@exchange = Honeymaker::Exchanges::Gemini.new
|
|
10
|
+
# Recorded, not slept: the pacing and the backoff are asserted as a sequence.
|
|
11
|
+
sleeps = @sleeps = []
|
|
12
|
+
@exchange.define_singleton_method(:sleep) { |seconds| sleeps << seconds }
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
def test_get_tickers_info_parses_response
|
|
@@ -38,6 +41,73 @@ class Honeymaker::Exchanges::GeminiTest < Minitest::Test
|
|
|
38
41
|
assert ticker[:trading_enabled]
|
|
39
42
|
end
|
|
40
43
|
|
|
44
|
+
def test_get_tickers_info_paces_every_request
|
|
45
|
+
stub_catalogue(%w[btcusd ethusd])
|
|
46
|
+
|
|
47
|
+
assert @exchange.get_tickers_info.success?
|
|
48
|
+
assert_equal [0.5, 0.5, 0.5], @sleeps
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_get_tickers_info_retries_a_dropped_detail_request
|
|
52
|
+
connection = stub_catalogue(%w[btcusd ethusd])
|
|
53
|
+
connection.stubs(:get).with("/v1/symbols/details/ethusd")
|
|
54
|
+
.raises(Faraday::ConnectionFailed, "end of file reached")
|
|
55
|
+
.then.returns(detail_response)
|
|
56
|
+
|
|
57
|
+
result = @exchange.get_tickers_info
|
|
58
|
+
|
|
59
|
+
assert result.success?
|
|
60
|
+
assert_equal 2, result.data.size
|
|
61
|
+
assert_equal [0.5, 0.5, 0.5, 2, 0.5], @sleeps
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_get_tickers_info_retries_the_symbols_request
|
|
65
|
+
connection = stub_catalogue(%w[btcusd])
|
|
66
|
+
connection.stubs(:get).with("/v1/symbols")
|
|
67
|
+
.raises(Faraday::TooManyRequestsError, "429")
|
|
68
|
+
.then.returns(stub(body: %w[btcusd]))
|
|
69
|
+
|
|
70
|
+
result = @exchange.get_tickers_info
|
|
71
|
+
|
|
72
|
+
assert result.success?
|
|
73
|
+
assert_equal 1, result.data.size
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_get_tickers_info_retries_every_transient_error_class
|
|
77
|
+
Honeymaker::Exchanges::Gemini::TRANSIENT_ERRORS.each do |error_class|
|
|
78
|
+
setup
|
|
79
|
+
connection = stub_catalogue(%w[btcusd])
|
|
80
|
+
connection.stubs(:get).with("/v1/symbols/details/btcusd")
|
|
81
|
+
.raises(error_class, "transient").then.returns(detail_response)
|
|
82
|
+
|
|
83
|
+
assert @exchange.get_tickers_info.success?, "#{error_class} should be retried"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# All or nothing: data-api reads a pair missing from the catalogue as delisted, so a symbol that
|
|
88
|
+
# keeps failing must fail the whole catalogue, and nothing after it is fetched.
|
|
89
|
+
def test_get_tickers_info_fails_whole_catalogue_when_a_symbol_keeps_failing
|
|
90
|
+
connection = stub_catalogue(%w[btcusd ethusd solusd])
|
|
91
|
+
connection.expects(:get).with("/v1/symbols/details/ethusd").times(3)
|
|
92
|
+
.raises(Faraday::ConnectionFailed, "end of file reached")
|
|
93
|
+
connection.expects(:get).with("/v1/symbols/details/solusd").never
|
|
94
|
+
|
|
95
|
+
result = @exchange.get_tickers_info
|
|
96
|
+
|
|
97
|
+
assert result.failure?
|
|
98
|
+
assert_nil result.data
|
|
99
|
+
assert_equal ["end of file reached"], result.errors
|
|
100
|
+
assert_equal [0.5, 0.5, 0.5, 2, 0.5, 4, 0.5], @sleeps
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_get_tickers_info_does_not_retry_a_non_transient_error
|
|
104
|
+
connection = stub_catalogue(%w[btcusd])
|
|
105
|
+
connection.expects(:get).with("/v1/symbols/details/btcusd").once
|
|
106
|
+
.raises(Faraday::ResourceNotFound, "404")
|
|
107
|
+
|
|
108
|
+
assert @exchange.get_tickers_info.failure?
|
|
109
|
+
end
|
|
110
|
+
|
|
41
111
|
def test_get_bid_ask_parses_response
|
|
42
112
|
body = load_fixture("gemini_pubticker.json")
|
|
43
113
|
stub_connection(body)
|
|
@@ -51,6 +121,19 @@ class Honeymaker::Exchanges::GeminiTest < Minitest::Test
|
|
|
51
121
|
|
|
52
122
|
private
|
|
53
123
|
|
|
124
|
+
def detail_response
|
|
125
|
+
stub(body: load_fixture("gemini_symbol_detail.json"))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Every symbol resolves to the one detail fixture; a test overrides a path to make it fail.
|
|
129
|
+
def stub_catalogue(symbols)
|
|
130
|
+
connection = stub
|
|
131
|
+
connection.stubs(:get).with("/v1/symbols").returns(stub(body: symbols))
|
|
132
|
+
connection.stubs(:get).with { |path| path.start_with?("/v1/symbols/details/") }.returns(detail_response)
|
|
133
|
+
@exchange.instance_variable_set(:@connection, connection)
|
|
134
|
+
connection
|
|
135
|
+
end
|
|
136
|
+
|
|
54
137
|
def stub_connection(body)
|
|
55
138
|
response = stub(body: body)
|
|
56
139
|
connection = stub
|