honeymaker 0.11.9 → 0.11.10
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
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 81ec4e430e403ef063e19f4c1a0827f9652bed081e7bdf845ba0f2c88b5c7a28
|
|
4
|
+
data.tar.gz: 779b9f7391e4259a711c478a2493445fecf88ef04bc46e64affdaf2b1267decb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2107a78f1bbec5167448470cfe8f26871c37fea3237be0ee1137d33cbe738ed9b23292ff99d35e1c9114f27a0634ba0aa315357cd126fd57f805919858a7c4eb
|
|
7
|
+
data.tar.gz: 1f694295c4ed6699728fd0fa500a5a081d1835b620a6b6d06e7ee3ab239804defe4bb8a9271ad638a0321e27aa5091dc6d5adbda5e69cfd97bdf89d9e310dd39
|
data/lib/honeymaker/exchange.rb
CHANGED
|
@@ -69,6 +69,15 @@ module Honeymaker
|
|
|
69
69
|
3600
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# Venue symbols the last #get_tickers_info saw listed but could not describe, symbol => why. They
|
|
73
|
+
# are missing from the catalogue WITHOUT being delisted: a caller that reads absence as delisting
|
|
74
|
+
# must keep what it has for them. Empty for a venue whose catalogue is a single request.
|
|
75
|
+
# It describes the last get_tickers_info call only; the #tickers_info cache does not carry it, so a
|
|
76
|
+
# caller that needs both reads them from one get_tickers_info.
|
|
77
|
+
def unreadable_symbols
|
|
78
|
+
@unreadable_symbols || {}
|
|
79
|
+
end
|
|
80
|
+
|
|
72
81
|
private
|
|
73
82
|
|
|
74
83
|
# No HTTP answer means the transport failed, and its text rarely says how ("end of file
|
|
@@ -78,6 +87,17 @@ module Honeymaker
|
|
|
78
87
|
status.nil? ? { status: nil, error_chain: Utils.error_chain(error) } : { status: status }
|
|
79
88
|
end
|
|
80
89
|
|
|
90
|
+
# Records one instrument the venue lists but whose description could not be read, keyed as the
|
|
91
|
+
# venue emits its ticker. The reason is what a person needs to see: the venue's own answer when it
|
|
92
|
+
# gave one ("HTTP 400 {...InvalidSymbol...}"), otherwise the exception.
|
|
93
|
+
def set_aside(symbol, error)
|
|
94
|
+
status = error.respond_to?(:response_status) ? error.response_status : nil
|
|
95
|
+
body = error.respond_to?(:response_body) ? error.response_body.to_s : ""
|
|
96
|
+
why = body.empty? ? "#{error.class}: #{error.message}" : body
|
|
97
|
+
(@unreadable_symbols ||= {})[symbol.to_s.upcase] = (status ? "HTTP #{status} #{why}" : why)[0, 200]
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
81
101
|
def with_rescue
|
|
82
102
|
Result::Success.new(yield)
|
|
83
103
|
rescue Faraday::Error => e
|
|
@@ -15,35 +15,39 @@ module Honeymaker
|
|
|
15
15
|
Faraday::TooManyRequestsError, Faraday::ServerError
|
|
16
16
|
].freeze
|
|
17
17
|
|
|
18
|
+
# A symbol still failing after its retries is usually the venue or the connection, not the
|
|
19
|
+
# instrument, and each costs up to ~100 s. Past this many in one run the venue itself is down, so
|
|
20
|
+
# the whole catalogue fails, as any failure did before.
|
|
21
|
+
# ponytail: flat per-run budget; raise it if a rollout ever 5xxs a bigger family than this.
|
|
22
|
+
NO_ANSWER_LIMIT = 10
|
|
23
|
+
|
|
24
|
+
# /v1/symbols says what Gemini lists; the details only describe it. A listed symbol whose details
|
|
25
|
+
# cannot be read this run is SET ASIDE: left out of the catalogue and named in
|
|
26
|
+
# #unreadable_symbols, which callers must read as "unknown", never as delisted. Gemini lists new
|
|
27
|
+
# symbols before it can describe them (details/gramsgd answered 400 InvalidSymbol for hours on
|
|
28
|
+
# 2026-09-22), and failing the whole catalogue for that one left every Gemini pair without a
|
|
29
|
+
# verdict.
|
|
18
30
|
def get_tickers_info
|
|
31
|
+
@unreadable_symbols = {}
|
|
19
32
|
with_rescue do
|
|
20
33
|
symbols = catalogue_get("/v1/symbols")
|
|
34
|
+
no_answers = 0
|
|
21
35
|
|
|
22
36
|
tickers = symbols.filter_map do |symbol|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
quote: detail["quote_currency"].upcase,
|
|
35
|
-
minimum_base_size: detail["min_order_size"],
|
|
36
|
-
minimum_quote_size: "0",
|
|
37
|
-
maximum_base_size: nil,
|
|
38
|
-
maximum_quote_size: nil,
|
|
39
|
-
base_decimals: Utils.decimals(tick_size),
|
|
40
|
-
quote_decimals: Utils.decimals(quote_increment),
|
|
41
|
-
price_decimals: Utils.decimals(quote_increment),
|
|
42
|
-
available: true,
|
|
43
|
-
trading_enabled: detail["status"] == "open"
|
|
44
|
-
}
|
|
37
|
+
describe(symbol, catalogue_get("/v1/symbols/details/#{symbol}"))
|
|
38
|
+
rescue *TRANSIENT_ERRORS => e
|
|
39
|
+
raise if (no_answers += 1) >= NO_ANSWER_LIMIT
|
|
40
|
+
|
|
41
|
+
set_aside(symbol, e)
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
set_aside(symbol, e)
|
|
44
|
+
end
|
|
45
|
+
tickers = set_aside_shared_pairs(tickers)
|
|
46
|
+
if tickers.empty? && @unreadable_symbols.any?
|
|
47
|
+
raise Error, "Gemini: no symbol could be read, e.g. #{@unreadable_symbols.first.join(': ')}"
|
|
45
48
|
end
|
|
46
|
-
|
|
49
|
+
|
|
50
|
+
tickers
|
|
47
51
|
end
|
|
48
52
|
end
|
|
49
53
|
|
|
@@ -64,23 +68,52 @@ module Honeymaker
|
|
|
64
68
|
@connection ||= build_connection(BASE_URL)
|
|
65
69
|
end
|
|
66
70
|
|
|
71
|
+
def describe(symbol, detail)
|
|
72
|
+
return unless spot?(symbol, detail)
|
|
73
|
+
|
|
74
|
+
tick_size = detail["tick_size"]&.to_s || "0.01"
|
|
75
|
+
quote_increment = detail["quote_increment"]&.to_s || "0.01"
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
ticker: symbol.upcase,
|
|
79
|
+
base: detail["base_currency"].upcase,
|
|
80
|
+
quote: detail["quote_currency"].upcase,
|
|
81
|
+
minimum_base_size: detail["min_order_size"],
|
|
82
|
+
minimum_quote_size: "0",
|
|
83
|
+
maximum_base_size: nil,
|
|
84
|
+
maximum_quote_size: nil,
|
|
85
|
+
base_decimals: Utils.decimals(tick_size),
|
|
86
|
+
quote_decimals: Utils.decimals(quote_increment),
|
|
87
|
+
price_decimals: Utils.decimals(quote_increment),
|
|
88
|
+
available: true,
|
|
89
|
+
trading_enabled: detail["status"] == "open"
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
67
93
|
# Gemini lists perpetual swaps beside spot, under the same base and quote (BTCGUSDPERP is
|
|
68
94
|
# BTC/GUSD, product_type "swap"), so only product_type tells them apart. An instrument without
|
|
69
|
-
# one is not assumed to be spot:
|
|
95
|
+
# one is not assumed to be spot: it is set aside. So is a body that is not an object -- JSON
|
|
96
|
+
# served as text/plain arrives as a String, and String#[] finds "product_type" inside it.
|
|
70
97
|
def spot?(symbol, detail)
|
|
98
|
+
raise Error, "Gemini #{symbol}: details are not an object" unless detail.is_a?(Hash)
|
|
99
|
+
|
|
71
100
|
product_type = detail["product_type"]
|
|
72
|
-
raise Error, "Gemini #{symbol}: no product_type in its details" unless product_type.is_a?(String)
|
|
101
|
+
raise Error, "Gemini #{symbol}: no product_type in its details" unless product_type.is_a?(String) && !product_type.empty?
|
|
102
|
+
# A missing status would otherwise read as "not open" and revoke the pair.
|
|
103
|
+
raise Error, "Gemini #{symbol}: no status in its details" unless detail["status"].is_a?(String)
|
|
73
104
|
|
|
74
105
|
product_type == "spot"
|
|
75
106
|
end
|
|
76
107
|
|
|
77
|
-
# One pair, one instrument. Two would leave callers to pick one by list order
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
108
|
+
# One pair, one instrument. Two would leave callers to pick one by list order, so neither is
|
|
109
|
+
# picked: both are set aside until Gemini lists only one.
|
|
110
|
+
def set_aside_shared_pairs(tickers)
|
|
111
|
+
shared = tickers.group_by { |t| [t[:base], t[:quote]] }.values.reject(&:one?)
|
|
112
|
+
shared.each do |group|
|
|
113
|
+
claim = "Gemini #{group.first[:base]}/#{group.first[:quote]} is claimed by #{group.map { |t| t[:ticker] }.join(' and ')}"
|
|
114
|
+
group.each { |t| @unreadable_symbols[t[:ticker]] = claim }
|
|
115
|
+
end
|
|
116
|
+
tickers - shared.flatten
|
|
84
117
|
end
|
|
85
118
|
|
|
86
119
|
def catalogue_get(path)
|
data/lib/honeymaker/version.rb
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
# The set-aside rule, driven through honeymaker's real middleware stack: one symbol Gemini lists but
|
|
6
|
+
# cannot describe is left out and named, and never fails the whole catalogue.
|
|
7
|
+
class Honeymaker::Exchanges::GeminiCatalogueTest < Minitest::Test
|
|
8
|
+
JSON_CT = { "Content-Type" => "application/json" }.freeze
|
|
9
|
+
|
|
10
|
+
def detail(sym, base, quote, **over)
|
|
11
|
+
{ "symbol" => sym.upcase, "base_currency" => base, "quote_currency" => quote, "tick_size" => "0.00000001",
|
|
12
|
+
"quote_increment" => "0.01", "min_order_size" => "0.00001", "status" => "open", "product_type" => "spot" }.merge(over.transform_keys(&:to_s))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def exchange_with(symbols, &details)
|
|
16
|
+
stubs = Faraday::Adapter::Test::Stubs.new do |s|
|
|
17
|
+
s.get("/v1/symbols") { [200, JSON_CT, JSON.dump(symbols)] }
|
|
18
|
+
symbols.each { |sym| s.get("/v1/symbols/details/#{sym}") { details.call(sym) } }
|
|
19
|
+
end
|
|
20
|
+
ex = Honeymaker::Exchanges::Gemini.new
|
|
21
|
+
@sleeps = sleeps = []
|
|
22
|
+
ex.define_singleton_method(:sleep) { |s| sleeps << s }
|
|
23
|
+
conn = Faraday.new(url: "https://api.gemini.com") do |c|
|
|
24
|
+
c.request :json; c.response :json; c.response :raise_error; c.adapter :test, stubs
|
|
25
|
+
end
|
|
26
|
+
ex.instance_variable_set(:@connection, conn)
|
|
27
|
+
ex
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ok(sym)
|
|
31
|
+
base = sym[0..-4].upcase
|
|
32
|
+
[200, JSON_CT, JSON.dump(detail(sym, base, "USD"))]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_incident_replay_sets_gramsgd_aside_and_reads_the_rest
|
|
36
|
+
body = '{"result":"error","reason":"InvalidSymbol","message":"Received unsupported symbol \'gramsgd\'"}'
|
|
37
|
+
ex = exchange_with(%w[btcusd gramsgd ethusd]) { |s| s == "gramsgd" ? [400, JSON_CT, body] : ok(s) }
|
|
38
|
+
r = ex.get_tickers_info
|
|
39
|
+
assert r.success?
|
|
40
|
+
assert_equal %w[BTCUSD ETHUSD], r.data.map { |t| t[:ticker] }
|
|
41
|
+
assert_equal({ "GRAMSGD" => "HTTP 400 #{body}" }, ex.unreadable_symbols)
|
|
42
|
+
assert_equal [0.5] * 4, @sleeps, "a 400 is not retried"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_text_plain_details_are_set_aside_not_read_as_non_spot
|
|
46
|
+
ex = exchange_with(%w[btcusd ethusd]) do |s|
|
|
47
|
+
s == "ethusd" ? [200, { "Content-Type" => "text/plain" }, JSON.dump(detail("ethusd", "ETH", "USD"))] : ok(s)
|
|
48
|
+
end
|
|
49
|
+
r = ex.get_tickers_info
|
|
50
|
+
assert r.success?
|
|
51
|
+
assert_equal %w[BTCUSD], r.data.map { |t| t[:ticker] }
|
|
52
|
+
assert_match(/not an object/, ex.unreadable_symbols["ETHUSD"])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_garbage_shapes_are_each_set_aside
|
|
56
|
+
shapes = {
|
|
57
|
+
"aaausd" => [200, JSON_CT, JSON.dump(detail("aaausd", "AAA", "USD").except("product_type"))],
|
|
58
|
+
"bbbusd" => [200, { "Content-Type" => "text/html" }, "<html>oops</html>"],
|
|
59
|
+
"cccusd" => [200, JSON_CT, "{not json"],
|
|
60
|
+
"dddusd" => [200, JSON_CT, JSON.dump([1, 2])],
|
|
61
|
+
"eeeusd" => [200, JSON_CT, JSON.dump(detail("eeeusd", nil, "USD"))],
|
|
62
|
+
"fffusd" => [403, { "Content-Type" => "text/html" }, "<html>blocked</html>"],
|
|
63
|
+
"gggusd" => [404, JSON_CT, "{}"],
|
|
64
|
+
"hhhusd" => [200, JSON_CT, JSON.dump(detail("hhhusd", "HHH", "USD", product_type: ""))],
|
|
65
|
+
"iiiusd" => [200, JSON_CT, JSON.dump(detail("iiiusd", "III", "USD").except("status"))]
|
|
66
|
+
}
|
|
67
|
+
ex = exchange_with(%w[btcusd] + shapes.keys) { |s| shapes[s] || ok(s) }
|
|
68
|
+
r = ex.get_tickers_info
|
|
69
|
+
assert r.success?, r.errors.inspect
|
|
70
|
+
assert_equal %w[BTCUSD], r.data.map { |t| t[:ticker] }
|
|
71
|
+
assert_equal shapes.keys.map(&:upcase).sort, ex.unreadable_symbols.keys.sort
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_one_symbol_without_answer_is_set_aside_after_its_retries
|
|
75
|
+
ex = exchange_with(%w[btcusd ethusd solusd]) { |s| s == "ethusd" ? [503, JSON_CT, "{}"] : ok(s) }
|
|
76
|
+
r = ex.get_tickers_info
|
|
77
|
+
assert r.success?
|
|
78
|
+
assert_equal %w[BTCUSD SOLUSD], r.data.map { |t| t[:ticker] }
|
|
79
|
+
assert_equal [0.5, 0.5, 0.5, 2, 0.5, 4, 0.5, 0.5], @sleeps
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_the_no_answer_budget_fails_the_whole_catalogue
|
|
83
|
+
syms = (1..12).map { |i| format("x%02dusd", i) }
|
|
84
|
+
requested = []
|
|
85
|
+
ex = exchange_with(syms) { |s| requested << s; [503, JSON_CT, "{}"] }
|
|
86
|
+
r = ex.get_tickers_info
|
|
87
|
+
assert r.failure?
|
|
88
|
+
assert_equal({ status: 503 }, r.data)
|
|
89
|
+
assert_equal 10, requested.uniq.size, "stops at the budget"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_nine_no_answers_among_readable_symbols_still_succeed
|
|
93
|
+
syms = (1..9).flat_map { |i| [format("x%02dusd", i), format("y%02dusd", i)] }
|
|
94
|
+
ex = exchange_with(syms) { |s| s.start_with?("x") ? [503, JSON_CT, "{}"] : ok(s) }
|
|
95
|
+
r = ex.get_tickers_info
|
|
96
|
+
assert r.success?, r.errors.inspect
|
|
97
|
+
assert_equal 9, r.data.size
|
|
98
|
+
assert_equal 9, ex.unreadable_symbols.size
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_every_pair_claimed_twice_is_set_aside_and_the_rest_kept
|
|
102
|
+
twins = { "btcusd2" => %w[BTC USD], "ethusd2" => %w[ETH USD] }
|
|
103
|
+
ex = exchange_with(%w[btcusd btcusd2 ethusd ethusd2 solusd]) do |s|
|
|
104
|
+
twins[s] ? [200, JSON_CT, JSON.dump(detail(s, *twins[s]))] : ok(s)
|
|
105
|
+
end
|
|
106
|
+
r = ex.get_tickers_info
|
|
107
|
+
assert r.success?
|
|
108
|
+
assert_equal %w[SOLUSD], r.data.map { |t| t[:ticker] }
|
|
109
|
+
assert_equal %w[BTCUSD BTCUSD2 ETHUSD ETHUSD2], ex.unreadable_symbols.keys.sort
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_nothing_readable_is_a_failure_and_state_resets_between_runs
|
|
113
|
+
ex = exchange_with(%w[btcusd]) { |_| [400, JSON_CT, "{}"] }
|
|
114
|
+
assert ex.get_tickers_info.failure?
|
|
115
|
+
assert_equal %w[BTCUSD], ex.unreadable_symbols.keys
|
|
116
|
+
assert_equal({}, Honeymaker::Exchanges::Binance.new.unreadable_symbols)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -76,28 +76,27 @@ class Honeymaker::Exchanges::GeminiTest < Minitest::Test
|
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
def test_get_tickers_info_fails_whole_catalogue_when_a_symbol_keeps_failing
|
|
79
|
+
# Set aside, not fatal: a symbol that keeps failing is left out and named, and the run goes on.
|
|
80
|
+
def test_get_tickers_info_sets_aside_a_symbol_that_keeps_failing
|
|
82
81
|
connection = stub_catalogue(%w[btcusd ethusd solusd])
|
|
83
82
|
connection.expects(:get).with("/v1/symbols/details/ethusd").times(3)
|
|
84
83
|
.raises(Faraday::ConnectionFailed, "end of file reached")
|
|
85
|
-
connection.expects(:get).with("/v1/symbols/details/solusd").never
|
|
86
84
|
|
|
87
85
|
result = @exchange.get_tickers_info
|
|
88
86
|
|
|
89
|
-
assert result.
|
|
90
|
-
|
|
91
|
-
assert_equal
|
|
92
|
-
assert_equal [0.5, 0.5, 0.5, 2, 0.5, 4, 0.5], @sleeps
|
|
87
|
+
assert result.success?
|
|
88
|
+
assert_equal %w[BTCUSD SOLUSD], result.data.map { |t| t[:ticker] }
|
|
89
|
+
assert_equal({ "ETHUSD" => "Faraday::ConnectionFailed: end of file reached" }, @exchange.unreadable_symbols)
|
|
90
|
+
assert_equal [0.5, 0.5, 0.5, 2, 0.5, 4, 0.5, 0.5], @sleeps
|
|
93
91
|
end
|
|
94
92
|
|
|
95
93
|
def test_get_tickers_info_does_not_retry_a_non_transient_error
|
|
96
|
-
connection = stub_catalogue(%w[btcusd])
|
|
94
|
+
connection = stub_catalogue(%w[btcusd ethusd])
|
|
97
95
|
connection.expects(:get).with("/v1/symbols/details/btcusd").once
|
|
98
96
|
.raises(Faraday::ResourceNotFound, "404")
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
assert_equal %w[ETHUSD], @exchange.get_tickers_info.data.map { |t| t[:ticker] }
|
|
99
|
+
assert_equal %w[BTCUSD], @exchange.unreadable_symbols.keys
|
|
101
100
|
end
|
|
102
101
|
|
|
103
102
|
# Gemini lists perpetual swaps in /v1/symbols with the same base and quote as the spot pair
|
|
@@ -113,29 +112,29 @@ class Honeymaker::Exchanges::GeminiTest < Minitest::Test
|
|
|
113
112
|
assert_equal %w[BTCUSD], result.data.map { |t| t[:ticker] }
|
|
114
113
|
end
|
|
115
114
|
|
|
116
|
-
# An unclassified instrument is not assumed to be spot:
|
|
117
|
-
#
|
|
118
|
-
def
|
|
115
|
+
# An unclassified instrument is not assumed to be spot: it is set aside, and callers keep what
|
|
116
|
+
# they have for it.
|
|
117
|
+
def test_get_tickers_info_sets_aside_a_symbol_with_no_product_type
|
|
119
118
|
connection = stub_catalogue(%w[btcusd ethusd])
|
|
120
119
|
connection.stubs(:get).with("/v1/symbols/details/ethusd")
|
|
121
120
|
.returns(stub(body: detail_body.except("product_type").merge("symbol" => "ETHUSD")))
|
|
122
121
|
|
|
123
122
|
result = @exchange.get_tickers_info
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
assert_match(/ethusd/, result.errors.first)
|
|
124
|
+
assert_equal %w[BTCUSD], result.data.map { |t| t[:ticker] }
|
|
125
|
+
assert_match(/ethusd: no product_type/, @exchange.unreadable_symbols["ETHUSD"])
|
|
128
126
|
end
|
|
129
127
|
|
|
130
|
-
def
|
|
131
|
-
connection = stub_catalogue(%w[btcusd btcusd2])
|
|
128
|
+
def test_get_tickers_info_sets_aside_both_instruments_claiming_one_pair
|
|
129
|
+
connection = stub_catalogue(%w[btcusd btcusd2 ethusd])
|
|
132
130
|
connection.stubs(:get).with("/v1/symbols/details/btcusd2")
|
|
133
131
|
.returns(stub(body: detail_body.merge("symbol" => "BTCUSD2")))
|
|
134
132
|
|
|
135
133
|
result = @exchange.get_tickers_info
|
|
136
134
|
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
assert_equal %w[ETHUSD], result.data.map { |t| t[:ticker] }
|
|
136
|
+
assert_equal %w[BTCUSD BTCUSD2], @exchange.unreadable_symbols.keys.sort
|
|
137
|
+
assert_match(%r{BTC/USD}, @exchange.unreadable_symbols["BTCUSD"])
|
|
139
138
|
end
|
|
140
139
|
|
|
141
140
|
def test_get_bid_ask_parses_response
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: honeymaker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.11.
|
|
4
|
+
version: 0.11.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Deltabadger
|
|
@@ -180,6 +180,7 @@ files:
|
|
|
180
180
|
- test/honeymaker/exchanges/bitvavo_test.rb
|
|
181
181
|
- test/honeymaker/exchanges/bybit_test.rb
|
|
182
182
|
- test/honeymaker/exchanges/coinbase_test.rb
|
|
183
|
+
- test/honeymaker/exchanges/gemini_catalogue_test.rb
|
|
183
184
|
- test/honeymaker/exchanges/gemini_test.rb
|
|
184
185
|
- test/honeymaker/exchanges/hyperliquid_test.rb
|
|
185
186
|
- test/honeymaker/exchanges/kraken_test.rb
|