honeymaker 0.12.4-aarch64-linux
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 +7 -0
- data/.github/workflows/native-gems.yml +100 -0
- data/.github/workflows/release.yml +36 -0
- data/.github/workflows/test.yml +33 -0
- data/LICENSE +21 -0
- data/README.md +219 -0
- data/Rakefile +78 -0
- data/crates/honeymaker/tests/vectors/decimal.json +146 -0
- data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
- data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
- data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
- data/lib/honeymaker/3.4/honeymaker_native.so +0 -0
- data/lib/honeymaker/4.0/honeymaker_native.so +0 -0
- data/lib/honeymaker/client.rb +139 -0
- data/lib/honeymaker/clients/binance.rb +687 -0
- data/lib/honeymaker/clients/binance_us.rb +42 -0
- data/lib/honeymaker/clients/bingx.rb +242 -0
- data/lib/honeymaker/clients/bitget.rb +318 -0
- data/lib/honeymaker/clients/bitrue.rb +245 -0
- data/lib/honeymaker/clients/bitvavo.rb +244 -0
- data/lib/honeymaker/clients/bybit.rb +308 -0
- data/lib/honeymaker/clients/coinbase.rb +326 -0
- data/lib/honeymaker/clients/gemini.rb +189 -0
- data/lib/honeymaker/clients/hyperliquid.rb +232 -0
- data/lib/honeymaker/clients/kraken.rb +379 -0
- data/lib/honeymaker/clients/kraken_futures.rb +78 -0
- data/lib/honeymaker/clients/kucoin.rb +310 -0
- data/lib/honeymaker/clients/mexc.rb +249 -0
- data/lib/honeymaker/exchange.rb +129 -0
- data/lib/honeymaker/exchanges/binance.rb +55 -0
- data/lib/honeymaker/exchanges/binance_us.rb +9 -0
- data/lib/honeymaker/exchanges/bingx.rb +56 -0
- data/lib/honeymaker/exchanges/bitget.rb +52 -0
- data/lib/honeymaker/exchanges/bitrue.rb +53 -0
- data/lib/honeymaker/exchanges/bitvavo.rb +65 -0
- data/lib/honeymaker/exchanges/bybit.rb +57 -0
- data/lib/honeymaker/exchanges/coinbase.rb +69 -0
- data/lib/honeymaker/exchanges/gemini.rb +134 -0
- data/lib/honeymaker/exchanges/hyperliquid.rb +67 -0
- data/lib/honeymaker/exchanges/kraken.rb +122 -0
- data/lib/honeymaker/exchanges/kucoin.rb +52 -0
- data/lib/honeymaker/exchanges/mexc.rb +56 -0
- data/lib/honeymaker/native/kraken.rb +263 -0
- data/lib/honeymaker/native.rb +42 -0
- data/lib/honeymaker/result.rb +46 -0
- data/lib/honeymaker/utils.rb +39 -0
- data/lib/honeymaker/version.rb +5 -0
- data/lib/honeymaker.rb +97 -0
- data/script/parity/kraken_client_live.rb +191 -0
- data/script/parity/kraken_live.rb +70 -0
- data/script/parity/kraken_transport.rb +604 -0
- data/script/parity/resolve.c +49 -0
- data/script/release/push_gems.sh +28 -0
- data/script/vectors/kraken.rb +50 -0
- data/script/vectors/kraken_stage2.rb +146 -0
- data/sig/honeymaker.rbs +4 -0
- data/test/fixtures/binance_book_ticker.json +7 -0
- data/test/fixtures/binance_exchange_info.json +30 -0
- data/test/fixtures/bingx_symbols.json +26 -0
- data/test/fixtures/bingx_ticker.json +12 -0
- data/test/fixtures/bitget_symbols.json +28 -0
- data/test/fixtures/bitget_tickers.json +14 -0
- data/test/fixtures/bitmart_symbols.json +26 -0
- data/test/fixtures/bitmart_ticker.json +12 -0
- data/test/fixtures/bitrue_book_ticker.json +7 -0
- data/test/fixtures/bitrue_exchange_info.json +34 -0
- data/test/fixtures/bitvavo_book_ticker.json +7 -0
- data/test/fixtures/bitvavo_markets.json +30 -0
- data/test/fixtures/bybit_instruments.json +23 -0
- data/test/fixtures/bybit_tickers.json +19 -0
- data/test/fixtures/coinbase_product.json +7 -0
- data/test/fixtures/coinbase_products.json +24 -0
- data/test/fixtures/gemini_pubticker.json +10 -0
- data/test/fixtures/gemini_symbol_detail.json +10 -0
- data/test/fixtures/gemini_symbols.json +1 -0
- data/test/fixtures/hyperliquid_all_mids.json +5 -0
- data/test/fixtures/hyperliquid_spot_meta.json +12 -0
- data/test/fixtures/kraken_asset_pairs.json +43 -0
- data/test/fixtures/kraken_ticker.json +16 -0
- data/test/fixtures/kucoin_orderbook_level1.json +10 -0
- data/test/fixtures/kucoin_symbols.json +17 -0
- data/test/fixtures/mexc_book_ticker.json +7 -0
- data/test/fixtures/mexc_exchange_info.json +64 -0
- data/test/honeymaker/client_test.rb +90 -0
- data/test/honeymaker/clients/binance_client_test.rb +151 -0
- data/test/honeymaker/clients/binance_us_client_test.rb +25 -0
- data/test/honeymaker/clients/bingx_client_test.rb +106 -0
- data/test/honeymaker/clients/bitget_client_test.rb +146 -0
- data/test/honeymaker/clients/bitrue_client_test.rb +69 -0
- data/test/honeymaker/clients/bitvavo_client_test.rb +156 -0
- data/test/honeymaker/clients/bybit_client_test.rb +137 -0
- data/test/honeymaker/clients/coinbase_client_test.rb +153 -0
- data/test/honeymaker/clients/gemini_client_test.rb +100 -0
- data/test/honeymaker/clients/honeymaker_client_registry_test.rb +39 -0
- data/test/honeymaker/clients/hyperliquid_client_test.rb +292 -0
- data/test/honeymaker/clients/kraken_client_test.rb +255 -0
- data/test/honeymaker/clients/kraken_futures_client_test.rb +54 -0
- data/test/honeymaker/clients/kucoin_client_test.rb +149 -0
- data/test/honeymaker/clients/mexc_client_test.rb +98 -0
- data/test/honeymaker/clients/validation_test.rb +167 -0
- data/test/honeymaker/error_chain_test.rb +133 -0
- data/test/honeymaker/exchange_test.rb +138 -0
- data/test/honeymaker/exchanges/binance_test.rb +83 -0
- data/test/honeymaker/exchanges/binance_us_test.rb +53 -0
- data/test/honeymaker/exchanges/bingx_test.rb +65 -0
- data/test/honeymaker/exchanges/bitget_test.rb +65 -0
- data/test/honeymaker/exchanges/bitrue_test.rb +65 -0
- data/test/honeymaker/exchanges/bitvavo_test.rb +65 -0
- data/test/honeymaker/exchanges/bybit_test.rb +55 -0
- data/test/honeymaker/exchanges/coinbase_test.rb +106 -0
- data/test/honeymaker/exchanges/gemini_catalogue_test.rb +118 -0
- data/test/honeymaker/exchanges/gemini_test.rb +184 -0
- data/test/honeymaker/exchanges/hyperliquid_test.rb +64 -0
- data/test/honeymaker/exchanges/kraken_test.rb +191 -0
- data/test/honeymaker/exchanges/kucoin_test.rb +55 -0
- data/test/honeymaker/exchanges/mexc_test.rb +97 -0
- data/test/honeymaker/honeymaker_test.rb +27 -0
- data/test/honeymaker/result_test.rb +42 -0
- data/test/honeymaker/utils_test.rb +64 -0
- data/test/native/backend_switch_test.rb +53 -0
- data/test/native/convert_test.rb +137 -0
- data/test/native/gem_isolation_test.rb +17 -0
- data/test/native/loader_test.rb +29 -0
- data/test/native/lockstep_test.rb +23 -0
- data/test/native/signing_smoke_test.rb +28 -0
- data/test/parity/kraken_client_parity_test.rb +398 -0
- data/test/parity/kraken_exchange_parity_test.rb +172 -0
- data/test/release/branch_hardening_test.rb +34 -0
- data/test/release/push_gems_test.rb +79 -0
- data/test/smoke.rb +24 -0
- data/test/support/fixture_helper.rb +13 -0
- data/test/support/kraken_signing_smoke.rb +21 -0
- data/test/support/parity_helper.rb +62 -0
- data/test/support/venue_double.rb +100 -0
- data/test/test_helper.rb +9 -0
- metadata +254 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::Exchanges::GeminiTest < Minitest::Test
|
|
6
|
+
include FixtureHelper
|
|
7
|
+
|
|
8
|
+
def setup
|
|
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 }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_get_tickers_info_parses_response
|
|
16
|
+
stub_catalogue(load_fixture("gemini_symbols.json"))
|
|
17
|
+
|
|
18
|
+
result = @exchange.get_tickers_info
|
|
19
|
+
|
|
20
|
+
assert result.success?
|
|
21
|
+
assert_equal 2, result.data.size
|
|
22
|
+
|
|
23
|
+
ticker = result.data.first
|
|
24
|
+
assert_equal "BTCUSD", ticker[:ticker]
|
|
25
|
+
assert_equal "BTC", ticker[:base]
|
|
26
|
+
assert_equal "USD", ticker[:quote]
|
|
27
|
+
assert_equal "0.00001", ticker[:minimum_base_size]
|
|
28
|
+
assert_equal "0", ticker[:minimum_quote_size]
|
|
29
|
+
assert_equal 8, ticker[:base_decimals]
|
|
30
|
+
assert_equal 2, ticker[:quote_decimals]
|
|
31
|
+
assert_equal 2, ticker[:price_decimals]
|
|
32
|
+
assert ticker[:available]
|
|
33
|
+
assert ticker[:trading_enabled]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_get_tickers_info_paces_every_request
|
|
37
|
+
stub_catalogue(%w[btcusd ethusd])
|
|
38
|
+
|
|
39
|
+
assert @exchange.get_tickers_info.success?
|
|
40
|
+
assert_equal [0.5, 0.5, 0.5], @sleeps
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_get_tickers_info_retries_a_dropped_detail_request
|
|
44
|
+
connection = stub_catalogue(%w[btcusd ethusd])
|
|
45
|
+
connection.stubs(:get).with("/v1/symbols/details/ethusd")
|
|
46
|
+
.raises(Faraday::ConnectionFailed, "end of file reached")
|
|
47
|
+
.then.returns(stub(body: detail_for("ethusd")))
|
|
48
|
+
|
|
49
|
+
result = @exchange.get_tickers_info
|
|
50
|
+
|
|
51
|
+
assert result.success?
|
|
52
|
+
assert_equal 2, result.data.size
|
|
53
|
+
assert_equal [0.5, 0.5, 0.5, 2, 0.5], @sleeps
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_get_tickers_info_retries_the_symbols_request
|
|
57
|
+
connection = stub_catalogue(%w[btcusd])
|
|
58
|
+
connection.stubs(:get).with("/v1/symbols")
|
|
59
|
+
.raises(Faraday::TooManyRequestsError, "429")
|
|
60
|
+
.then.returns(stub(body: %w[btcusd]))
|
|
61
|
+
|
|
62
|
+
result = @exchange.get_tickers_info
|
|
63
|
+
|
|
64
|
+
assert result.success?
|
|
65
|
+
assert_equal 1, result.data.size
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_get_tickers_info_retries_every_transient_error_class
|
|
69
|
+
Honeymaker::Exchanges::Gemini::TRANSIENT_ERRORS.each do |error_class|
|
|
70
|
+
setup
|
|
71
|
+
connection = stub_catalogue(%w[btcusd])
|
|
72
|
+
connection.stubs(:get).with("/v1/symbols/details/btcusd")
|
|
73
|
+
.raises(error_class, "transient").then.returns(detail_response)
|
|
74
|
+
|
|
75
|
+
assert @exchange.get_tickers_info.success?, "#{error_class} should be retried"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
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
|
|
81
|
+
connection = stub_catalogue(%w[btcusd ethusd solusd])
|
|
82
|
+
connection.expects(:get).with("/v1/symbols/details/ethusd").times(3)
|
|
83
|
+
.raises(Faraday::ConnectionFailed, "end of file reached")
|
|
84
|
+
|
|
85
|
+
result = @exchange.get_tickers_info
|
|
86
|
+
|
|
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
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_get_tickers_info_does_not_retry_a_non_transient_error
|
|
94
|
+
connection = stub_catalogue(%w[btcusd ethusd])
|
|
95
|
+
connection.expects(:get).with("/v1/symbols/details/btcusd").once
|
|
96
|
+
.raises(Faraday::ResourceNotFound, "404")
|
|
97
|
+
|
|
98
|
+
assert_equal %w[ETHUSD], @exchange.get_tickers_info.data.map { |t| t[:ticker] }
|
|
99
|
+
assert_equal %w[BTCUSD], @exchange.unreadable_symbols.keys
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Gemini lists perpetual swaps in /v1/symbols with the same base and quote as the spot pair
|
|
103
|
+
# (BTCGUSDPERP is BTC/GUSD). They are not spot instruments, and would take the spot pair's place.
|
|
104
|
+
def test_get_tickers_info_keeps_only_spot_instruments
|
|
105
|
+
connection = stub_catalogue(%w[btcusd btcusdperp])
|
|
106
|
+
connection.stubs(:get).with("/v1/symbols/details/btcusdperp")
|
|
107
|
+
.returns(stub(body: detail_body.merge("symbol" => "BTCUSDPERP", "product_type" => "swap")))
|
|
108
|
+
|
|
109
|
+
result = @exchange.get_tickers_info
|
|
110
|
+
|
|
111
|
+
assert result.success?
|
|
112
|
+
assert_equal %w[BTCUSD], result.data.map { |t| t[:ticker] }
|
|
113
|
+
end
|
|
114
|
+
|
|
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
|
|
118
|
+
connection = stub_catalogue(%w[btcusd ethusd])
|
|
119
|
+
connection.stubs(:get).with("/v1/symbols/details/ethusd")
|
|
120
|
+
.returns(stub(body: detail_body.except("product_type").merge("symbol" => "ETHUSD")))
|
|
121
|
+
|
|
122
|
+
result = @exchange.get_tickers_info
|
|
123
|
+
|
|
124
|
+
assert_equal %w[BTCUSD], result.data.map { |t| t[:ticker] }
|
|
125
|
+
assert_match(/ethusd: no product_type/, @exchange.unreadable_symbols["ETHUSD"])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_get_tickers_info_sets_aside_both_instruments_claiming_one_pair
|
|
129
|
+
connection = stub_catalogue(%w[btcusd btcusd2 ethusd])
|
|
130
|
+
connection.stubs(:get).with("/v1/symbols/details/btcusd2")
|
|
131
|
+
.returns(stub(body: detail_body.merge("symbol" => "BTCUSD2")))
|
|
132
|
+
|
|
133
|
+
result = @exchange.get_tickers_info
|
|
134
|
+
|
|
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"])
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def test_get_bid_ask_parses_response
|
|
141
|
+
body = load_fixture("gemini_pubticker.json")
|
|
142
|
+
stub_connection(body)
|
|
143
|
+
|
|
144
|
+
result = @exchange.get_bid_ask("BTCUSD")
|
|
145
|
+
|
|
146
|
+
assert result.success?
|
|
147
|
+
assert_equal BigDecimal("67123.45"), result.data[:bid]
|
|
148
|
+
assert_equal BigDecimal("67125.67"), result.data[:ask]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def detail_body
|
|
154
|
+
load_fixture("gemini_symbol_detail.json")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def detail_response
|
|
158
|
+
stub(body: detail_body)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Every symbol resolves to the one detail fixture; a test overrides a path to make it fail.
|
|
162
|
+
# Each symbol answers with the fixture's details for its own pair ("ethusd" is ETH/USD), since a
|
|
163
|
+
# catalogue may not hold two instruments for one pair.
|
|
164
|
+
def stub_catalogue(symbols)
|
|
165
|
+
connection = stub
|
|
166
|
+
connection.stubs(:get).with("/v1/symbols").returns(stub(body: symbols))
|
|
167
|
+
symbols.each do |symbol|
|
|
168
|
+
connection.stubs(:get).with("/v1/symbols/details/#{symbol}").returns(stub(body: detail_for(symbol)))
|
|
169
|
+
end
|
|
170
|
+
@exchange.instance_variable_set(:@connection, connection)
|
|
171
|
+
connection
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def detail_for(symbol)
|
|
175
|
+
detail_body.merge("symbol" => symbol.upcase, "base_currency" => symbol[0, 3], "quote_currency" => symbol[3..])
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def stub_connection(body)
|
|
179
|
+
response = stub(body: body)
|
|
180
|
+
connection = stub
|
|
181
|
+
connection.stubs(:get).returns(response)
|
|
182
|
+
@exchange.instance_variable_set(:@connection, connection)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::Exchanges::HyperliquidTest < Minitest::Test
|
|
6
|
+
include FixtureHelper
|
|
7
|
+
|
|
8
|
+
def setup
|
|
9
|
+
@exchange = Honeymaker::Exchanges::Hyperliquid.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_get_tickers_info_parses_response
|
|
13
|
+
body = load_fixture("hyperliquid_spot_meta.json")
|
|
14
|
+
stub_connection(body)
|
|
15
|
+
|
|
16
|
+
result = @exchange.get_tickers_info
|
|
17
|
+
|
|
18
|
+
assert result.success?
|
|
19
|
+
assert_equal 1, result.data.size
|
|
20
|
+
|
|
21
|
+
ticker = result.data.first
|
|
22
|
+
assert_equal "PURR/USDC", ticker[:ticker]
|
|
23
|
+
assert_equal "PURR", ticker[:base]
|
|
24
|
+
assert_equal "USDC", ticker[:quote]
|
|
25
|
+
assert_nil ticker[:minimum_base_size] # no per-token base floor in spotMeta
|
|
26
|
+
assert_equal 10, ticker[:minimum_quote_size] # Hyperliquid's hard 10-USDC spot order floor
|
|
27
|
+
assert_equal 2, ticker[:base_decimals]
|
|
28
|
+
assert_equal 2, ticker[:quote_decimals]
|
|
29
|
+
assert_equal 5, ticker[:price_decimals]
|
|
30
|
+
assert ticker[:available]
|
|
31
|
+
assert ticker[:trading_enabled] # no native status -> always true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_skips_pairs_with_missing_tokens
|
|
35
|
+
body = load_fixture("hyperliquid_spot_meta.json")
|
|
36
|
+
body["universe"] << { "name" => "MISSING/PAIR", "tokens" => [99, 100] }
|
|
37
|
+
stub_connection(body)
|
|
38
|
+
|
|
39
|
+
result = @exchange.get_tickers_info
|
|
40
|
+
|
|
41
|
+
assert result.success?
|
|
42
|
+
assert_equal 1, result.data.size
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_get_bid_ask_parses_response
|
|
46
|
+
body = load_fixture("hyperliquid_all_mids.json")
|
|
47
|
+
stub_connection(body)
|
|
48
|
+
|
|
49
|
+
result = @exchange.get_bid_ask("BTC/USDC")
|
|
50
|
+
|
|
51
|
+
assert result.success?
|
|
52
|
+
assert_equal BigDecimal("67124.56"), result.data[:bid]
|
|
53
|
+
assert_equal BigDecimal("67124.56"), result.data[:ask]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def stub_connection(body)
|
|
59
|
+
response = stub(body: body)
|
|
60
|
+
connection = stub
|
|
61
|
+
connection.stubs(:post).with { |_, &block| block&.call(OpenStruct.new(body: nil)); true }.returns(response)
|
|
62
|
+
@exchange.instance_variable_set(:@connection, connection)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::Exchanges::KrakenTest < Minitest::Test
|
|
6
|
+
include FixtureHelper
|
|
7
|
+
|
|
8
|
+
def setup
|
|
9
|
+
@exchange = Honeymaker::Exchanges::Kraken.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Tokenized equities (xStocks) live behind aclass_base and are invisible to a bare AssetPairs call.
|
|
13
|
+
# aclass_base=all returns currency + tokenized in one request; the two sets are disjoint.
|
|
14
|
+
def test_get_tickers_info_requests_every_asset_class
|
|
15
|
+
response = stub(body: load_fixture("kraken_asset_pairs.json"))
|
|
16
|
+
connection = stub
|
|
17
|
+
connection.expects(:get).with("/0/public/AssetPairs", { aclass_base: "all" }).returns(response)
|
|
18
|
+
@exchange.instance_variable_set(:@connection, connection)
|
|
19
|
+
|
|
20
|
+
assert @exchange.get_tickers_info.success?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Kraken returns every tokenized pair TWICE - once under an SPV key, once under the x key - with
|
|
24
|
+
# one shared wsname and altname. Only tokenized pairs are double-keyed; currency pairs never are.
|
|
25
|
+
# Mapping the raw response would ingest each of them twice.
|
|
26
|
+
def test_get_tickers_info_deduplicates_the_spv_alias
|
|
27
|
+
body = load_fixture("kraken_asset_pairs.json")
|
|
28
|
+
stub_request(body)
|
|
29
|
+
|
|
30
|
+
result = @exchange.get_tickers_info
|
|
31
|
+
|
|
32
|
+
nvda = result.data.select { |t| t[:base] == "NVDAx" }
|
|
33
|
+
assert_equal 1, nvda.size, "NVDAxUSD and NVDASPVUSD are one pair"
|
|
34
|
+
assert_equal "NVDAxUSD", nvda.first[:ticker]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Listed, so a holding can be resolved and valued; not tradable, because AddOrder needs an
|
|
38
|
+
# asset_class parameter this client does not send, and Kraken closes the tokenized order books to
|
|
39
|
+
# EEA clients over the API regardless.
|
|
40
|
+
def test_tokenized_pairs_are_listed_but_not_trading_enabled
|
|
41
|
+
body = load_fixture("kraken_asset_pairs.json")
|
|
42
|
+
stub_request(body)
|
|
43
|
+
|
|
44
|
+
nvda = @exchange.get_tickers_info.data.find { |t| t[:base] == "NVDAx" }
|
|
45
|
+
|
|
46
|
+
assert_equal "online", body["result"]["NVDAxUSD"]["status"]
|
|
47
|
+
assert nvda[:available], "listed, so balances resolve"
|
|
48
|
+
refute nvda[:trading_enabled], "we cannot place these orders"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_get_tickers_info_parses_response
|
|
52
|
+
body = load_fixture("kraken_asset_pairs.json")
|
|
53
|
+
stub_request(body)
|
|
54
|
+
|
|
55
|
+
result = @exchange.get_tickers_info
|
|
56
|
+
|
|
57
|
+
assert result.success?
|
|
58
|
+
ticker = result.data.first
|
|
59
|
+
assert_equal "XBTUSDT", ticker[:ticker]
|
|
60
|
+
assert_equal "XBT", ticker[:base]
|
|
61
|
+
assert_equal "USDT", ticker[:quote]
|
|
62
|
+
assert_equal "0.00010000", ticker[:minimum_base_size]
|
|
63
|
+
assert_equal "5", ticker[:minimum_quote_size]
|
|
64
|
+
assert_equal 8, ticker[:base_decimals]
|
|
65
|
+
assert_equal 5, ticker[:quote_decimals]
|
|
66
|
+
assert_equal 1, ticker[:price_decimals]
|
|
67
|
+
assert ticker[:available]
|
|
68
|
+
assert ticker[:trading_enabled] # fixture has no status -> defaults to true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_get_tickers_info_disables_non_online_status
|
|
72
|
+
body = load_fixture("kraken_asset_pairs.json")
|
|
73
|
+
body["result"]["XBTUSDT"]["status"] = "cancel_only"
|
|
74
|
+
stub_request(body)
|
|
75
|
+
|
|
76
|
+
result = @exchange.get_tickers_info
|
|
77
|
+
|
|
78
|
+
ticker = result.data.first
|
|
79
|
+
assert ticker[:available] # still listed
|
|
80
|
+
refute ticker[:trading_enabled] # but not trading
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_get_tickers_info_enables_online_status
|
|
84
|
+
body = load_fixture("kraken_asset_pairs.json")
|
|
85
|
+
body["result"]["XBTUSDT"]["status"] = "online"
|
|
86
|
+
stub_request(body)
|
|
87
|
+
|
|
88
|
+
result = @exchange.get_tickers_info
|
|
89
|
+
|
|
90
|
+
assert result.data.first[:trading_enabled]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_get_tickers_info_defaults_trading_enabled_when_status_absent
|
|
94
|
+
body = load_fixture("kraken_asset_pairs.json") # no status key
|
|
95
|
+
stub_request(body)
|
|
96
|
+
|
|
97
|
+
result = @exchange.get_tickers_info
|
|
98
|
+
|
|
99
|
+
assert result.data.first[:trading_enabled]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_get_tickers_info_skips_pairs_without_wsname
|
|
103
|
+
body = load_fixture("kraken_asset_pairs.json")
|
|
104
|
+
body["result"]["XBTUSDT"]["wsname"] = nil
|
|
105
|
+
stub_request(body)
|
|
106
|
+
|
|
107
|
+
result = @exchange.get_tickers_info
|
|
108
|
+
|
|
109
|
+
assert result.success?
|
|
110
|
+
# Asserts the wsname-less pair is dropped, not that the whole response is - the fixture now
|
|
111
|
+
# carries a tokenized pair too.
|
|
112
|
+
refute result.data.any? { |t| t[:ticker] == "XBTUSDT" }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_get_tickers_info_uses_real_costmin
|
|
116
|
+
body = load_fixture("kraken_asset_pairs.json")
|
|
117
|
+
stub_request(body)
|
|
118
|
+
|
|
119
|
+
result = @exchange.get_tickers_info
|
|
120
|
+
|
|
121
|
+
ticker = result.data.first
|
|
122
|
+
# USDT is in REAL_COSTMIN with value 5
|
|
123
|
+
assert_equal "5", ticker[:minimum_quote_size]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_get_tickers_info_handles_api_error
|
|
127
|
+
body = { "error" => ["EGeneral:Internal error"], "result" => {} }
|
|
128
|
+
stub_request(body)
|
|
129
|
+
|
|
130
|
+
result = @exchange.get_tickers_info
|
|
131
|
+
|
|
132
|
+
assert result.failure?
|
|
133
|
+
assert_includes result.errors, "EGeneral:Internal error"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_get_bid_ask_parses_response
|
|
137
|
+
body = load_fixture("kraken_ticker.json")
|
|
138
|
+
stub_request(body)
|
|
139
|
+
|
|
140
|
+
result = @exchange.get_bid_ask("XBTUSDT")
|
|
141
|
+
|
|
142
|
+
assert result.success?
|
|
143
|
+
assert_equal BigDecimal("67123.45"), result.data[:bid]
|
|
144
|
+
assert_equal BigDecimal("67125.67"), result.data[:ask]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def test_get_bid_ask_handles_api_error
|
|
148
|
+
body = { "error" => ["EGeneral:Internal error"], "result" => {} }
|
|
149
|
+
stub_request(body)
|
|
150
|
+
|
|
151
|
+
result = @exchange.get_bid_ask("XBTUSDT")
|
|
152
|
+
|
|
153
|
+
assert result.failure?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def test_classify_error_regional_restriction
|
|
157
|
+
result = @exchange.classify_error("EAccount:Invalid permissions:XAUT trading restricted for DK.")
|
|
158
|
+
assert_equal({ code: :regional_restriction, asset: "XAUT", country: "DK" }, result)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def test_classify_error_invalid_nonce
|
|
162
|
+
assert_equal({ code: :transient_nonce }, @exchange.classify_error("EAPI:Invalid nonce"))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_classify_error_internal_error_is_transient_unavailable
|
|
166
|
+
assert_equal({ code: :transient_unavailable }, @exchange.classify_error("EGeneral:Internal error"))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def test_classify_error_service_codes_are_transient_unavailable
|
|
170
|
+
assert_equal({ code: :transient_unavailable }, @exchange.classify_error("EService:Unavailable"))
|
|
171
|
+
assert_equal({ code: :transient_unavailable }, @exchange.classify_error("EService:Busy"))
|
|
172
|
+
assert_equal({ code: :transient_unavailable }, @exchange.classify_error("EService:Deadline elapsed"))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def test_classify_error_returns_nil_for_unknown_message
|
|
176
|
+
assert_nil @exchange.classify_error("EOrder:Unknown order")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def test_classify_error_returns_nil_for_nil_message
|
|
180
|
+
assert_nil @exchange.classify_error(nil)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def stub_request(body)
|
|
186
|
+
response = stub(body: body)
|
|
187
|
+
connection = stub
|
|
188
|
+
connection.stubs(:get).returns(response)
|
|
189
|
+
@exchange.instance_variable_set(:@connection, connection)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::Exchanges::KucoinTest < Minitest::Test
|
|
6
|
+
include FixtureHelper
|
|
7
|
+
|
|
8
|
+
def setup
|
|
9
|
+
@exchange = Honeymaker::Exchanges::Kucoin.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_get_tickers_info_parses_response
|
|
13
|
+
body = load_fixture("kucoin_symbols.json")
|
|
14
|
+
stub_connection(body)
|
|
15
|
+
|
|
16
|
+
result = @exchange.get_tickers_info
|
|
17
|
+
|
|
18
|
+
assert result.success?
|
|
19
|
+
assert_equal 1, result.data.size
|
|
20
|
+
|
|
21
|
+
ticker = result.data.first
|
|
22
|
+
assert_equal "BTC-USDT", ticker[:ticker]
|
|
23
|
+
assert_equal "BTC", ticker[:base]
|
|
24
|
+
assert_equal "USDT", ticker[:quote]
|
|
25
|
+
assert_equal "0.00001", ticker[:minimum_base_size]
|
|
26
|
+
assert_equal "0.1", ticker[:minimum_quote_size]
|
|
27
|
+
assert_equal "10000000000", ticker[:maximum_base_size]
|
|
28
|
+
assert_equal "99999999", ticker[:maximum_quote_size]
|
|
29
|
+
assert_equal 8, ticker[:base_decimals]
|
|
30
|
+
assert_equal 6, ticker[:quote_decimals]
|
|
31
|
+
assert_equal 1, ticker[:price_decimals]
|
|
32
|
+
assert ticker[:available]
|
|
33
|
+
assert ticker[:trading_enabled]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_get_bid_ask_parses_response
|
|
37
|
+
body = load_fixture("kucoin_orderbook_level1.json")
|
|
38
|
+
stub_connection(body)
|
|
39
|
+
|
|
40
|
+
result = @exchange.get_bid_ask("BTC-USDT")
|
|
41
|
+
|
|
42
|
+
assert result.success?
|
|
43
|
+
assert_equal BigDecimal("67123.45"), result.data[:bid]
|
|
44
|
+
assert_equal BigDecimal("67125.67"), result.data[:ask]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def stub_connection(body)
|
|
50
|
+
response = stub(body: body)
|
|
51
|
+
connection = stub
|
|
52
|
+
connection.stubs(:get).returns(response)
|
|
53
|
+
@exchange.instance_variable_set(:@connection, connection)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "digest"
|
|
5
|
+
|
|
6
|
+
class Honeymaker::Exchanges::MexcTest < Minitest::Test
|
|
7
|
+
include FixtureHelper
|
|
8
|
+
|
|
9
|
+
def setup
|
|
10
|
+
@exchange = Honeymaker::Exchanges::Mexc.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# H1. MEXC's exchangeInfo has never carried Binance's status encoding. The fixture this file
|
|
14
|
+
# drives was a hand-edited clone of the Binance one ("TRADING"/"HALT", LOT_SIZE/PRICE_FILTER/
|
|
15
|
+
# MIN_NOTIONAL filters) written from the same wrong assumption as the parser it tested, so both
|
|
16
|
+
# agreed and both were wrong. Asserting the raw bytes is the only guard that survives an author
|
|
17
|
+
# who believes the wrong thing twice; the digest also binds this file to deltabadger's copy, which
|
|
18
|
+
# drives the same capture through the other implementation of this contract.
|
|
19
|
+
def test_fixture_carries_mexcs_own_status_encoding
|
|
20
|
+
body = load_fixture("mexc_exchange_info.json")
|
|
21
|
+
|
|
22
|
+
assert_equal "1", body["symbols"].first["status"]
|
|
23
|
+
refute body["symbols"].any? { |s| s["status"] == "TRADING" }
|
|
24
|
+
refute body["symbols"].any? { |s| s["filters"].any? { |f| f["filterType"] == "LOT_SIZE" } }
|
|
25
|
+
|
|
26
|
+
assert_equal "13a65c0c8ebde52fac4cdca9a5be9d9a18ba7b5438c496b1b30606cf52fd60d3",
|
|
27
|
+
Digest::SHA256.hexdigest(File.read(fixture_path("mexc_exchange_info.json")))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_get_tickers_info_parses_response
|
|
31
|
+
body = load_fixture("mexc_exchange_info.json")
|
|
32
|
+
stub_connection(body)
|
|
33
|
+
|
|
34
|
+
result = @exchange.get_tickers_info
|
|
35
|
+
|
|
36
|
+
assert result.success?
|
|
37
|
+
assert_equal 2, result.data.size
|
|
38
|
+
|
|
39
|
+
ticker = result.data.first
|
|
40
|
+
assert_equal "METALUSDT", ticker[:ticker]
|
|
41
|
+
assert_equal "METAL", ticker[:base]
|
|
42
|
+
assert_equal "USDT", ticker[:quote]
|
|
43
|
+
assert ticker[:available]
|
|
44
|
+
assert ticker[:trading_enabled]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# H2. status is "1" for every MEXC symbol, including the 103 the venue will not accept a spot
|
|
48
|
+
# order for. The status check alone re-enables all of them; isSpotTradingAllowed is what keeps
|
|
49
|
+
# them out of the picker.
|
|
50
|
+
def test_a_symbol_mexc_does_not_allow_for_spot_is_not_trading_enabled
|
|
51
|
+
body = load_fixture("mexc_exchange_info.json")
|
|
52
|
+
stub_connection(body)
|
|
53
|
+
|
|
54
|
+
result = @exchange.get_tickers_info
|
|
55
|
+
|
|
56
|
+
disallowed = result.data.find { |t| t[:ticker] == "PALMAIUSDT" }
|
|
57
|
+
assert_equal "1", body["symbols"].last["status"]
|
|
58
|
+
assert disallowed[:available] # still listed
|
|
59
|
+
refute disallowed[:trading_enabled] # but not spot-tradable
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# H5. Real MEXC sends only PERCENT_PRICE_BY_SIDE, so the precision fallback is the ONLY path —
|
|
63
|
+
# there is no filter-bearing symbol to contrast it against.
|
|
64
|
+
def test_falls_back_to_precision_fields_when_no_size_filters
|
|
65
|
+
body = load_fixture("mexc_exchange_info.json")
|
|
66
|
+
stub_connection(body)
|
|
67
|
+
|
|
68
|
+
result = @exchange.get_tickers_info
|
|
69
|
+
|
|
70
|
+
metal = result.data.find { |t| t[:ticker] == "METALUSDT" }
|
|
71
|
+
assert_equal 2, metal[:base_decimals] # baseAssetPrecision
|
|
72
|
+
assert_equal 5, metal[:quote_decimals] # quoteAssetPrecision
|
|
73
|
+
assert_equal 5, metal[:price_decimals] # quotePrecision
|
|
74
|
+
assert_nil metal[:minimum_base_size]
|
|
75
|
+
assert_nil metal[:minimum_quote_size]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_get_bid_ask_parses_response
|
|
79
|
+
body = load_fixture("mexc_book_ticker.json")
|
|
80
|
+
stub_connection(body)
|
|
81
|
+
|
|
82
|
+
result = @exchange.get_bid_ask("BTCUSDT")
|
|
83
|
+
|
|
84
|
+
assert result.success?
|
|
85
|
+
assert_equal BigDecimal("67123.45"), result.data[:bid]
|
|
86
|
+
assert_equal BigDecimal("67125.67"), result.data[:ask]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def stub_connection(body)
|
|
92
|
+
response = stub(body: body)
|
|
93
|
+
connection = stub
|
|
94
|
+
connection.stubs(:get).returns(response)
|
|
95
|
+
@exchange.instance_variable_set(:@connection, connection)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class HoneymakerTest < Minitest::Test
|
|
6
|
+
def test_version
|
|
7
|
+
refute_nil Honeymaker::VERSION
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_exchange_returns_correct_class
|
|
11
|
+
exchange = Honeymaker.exchange("binance")
|
|
12
|
+
assert_instance_of Honeymaker::Exchanges::Binance, exchange
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_exchange_with_symbol
|
|
16
|
+
exchange = Honeymaker.exchange(:kraken)
|
|
17
|
+
assert_instance_of Honeymaker::Exchanges::Kraken, exchange
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_exchange_raises_for_unknown
|
|
21
|
+
assert_raises(Honeymaker::Error) { Honeymaker.exchange("unknown") }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_all_exchanges_registered
|
|
25
|
+
assert_equal 13, Honeymaker::EXCHANGES.size
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::ResultTest < Minitest::Test
|
|
6
|
+
def test_success_is_successful
|
|
7
|
+
result = Honeymaker::Result::Success.new("data")
|
|
8
|
+
assert result.success?
|
|
9
|
+
refute result.failure?
|
|
10
|
+
assert_equal "data", result.data
|
|
11
|
+
assert_empty result.errors
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_success_with_nil_data
|
|
15
|
+
result = Honeymaker::Result::Success.new
|
|
16
|
+
assert result.success?
|
|
17
|
+
assert_nil result.data
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_failure_is_not_successful
|
|
21
|
+
result = Honeymaker::Result::Failure.new("something went wrong")
|
|
22
|
+
refute result.success?
|
|
23
|
+
assert result.failure?
|
|
24
|
+
assert_equal ["something went wrong"], result.errors
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_failure_with_multiple_errors
|
|
28
|
+
result = Honeymaker::Result::Failure.new("error1", "error2")
|
|
29
|
+
assert_equal ["error1", "error2"], result.errors
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_failure_with_default_error
|
|
33
|
+
result = Honeymaker::Result::Failure.new
|
|
34
|
+
assert_equal ["Error"], result.errors
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_failure_with_data
|
|
38
|
+
result = Honeymaker::Result::Failure.new("err", data: { partial: true })
|
|
39
|
+
assert_equal({ partial: true }, result.data)
|
|
40
|
+
assert_equal ["err"], result.errors
|
|
41
|
+
end
|
|
42
|
+
end
|