honeymaker 0.12.4-x86_64-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.
Files changed (136) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/native-gems.yml +100 -0
  3. data/.github/workflows/release.yml +36 -0
  4. data/.github/workflows/test.yml +33 -0
  5. data/LICENSE +21 -0
  6. data/README.md +219 -0
  7. data/Rakefile +78 -0
  8. data/crates/honeymaker/tests/vectors/decimal.json +146 -0
  9. data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
  10. data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
  11. data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
  12. data/lib/honeymaker/3.4/honeymaker_native.so +0 -0
  13. data/lib/honeymaker/4.0/honeymaker_native.so +0 -0
  14. data/lib/honeymaker/client.rb +139 -0
  15. data/lib/honeymaker/clients/binance.rb +687 -0
  16. data/lib/honeymaker/clients/binance_us.rb +42 -0
  17. data/lib/honeymaker/clients/bingx.rb +242 -0
  18. data/lib/honeymaker/clients/bitget.rb +318 -0
  19. data/lib/honeymaker/clients/bitrue.rb +245 -0
  20. data/lib/honeymaker/clients/bitvavo.rb +244 -0
  21. data/lib/honeymaker/clients/bybit.rb +308 -0
  22. data/lib/honeymaker/clients/coinbase.rb +326 -0
  23. data/lib/honeymaker/clients/gemini.rb +189 -0
  24. data/lib/honeymaker/clients/hyperliquid.rb +232 -0
  25. data/lib/honeymaker/clients/kraken.rb +379 -0
  26. data/lib/honeymaker/clients/kraken_futures.rb +78 -0
  27. data/lib/honeymaker/clients/kucoin.rb +310 -0
  28. data/lib/honeymaker/clients/mexc.rb +249 -0
  29. data/lib/honeymaker/exchange.rb +129 -0
  30. data/lib/honeymaker/exchanges/binance.rb +55 -0
  31. data/lib/honeymaker/exchanges/binance_us.rb +9 -0
  32. data/lib/honeymaker/exchanges/bingx.rb +56 -0
  33. data/lib/honeymaker/exchanges/bitget.rb +52 -0
  34. data/lib/honeymaker/exchanges/bitrue.rb +53 -0
  35. data/lib/honeymaker/exchanges/bitvavo.rb +65 -0
  36. data/lib/honeymaker/exchanges/bybit.rb +57 -0
  37. data/lib/honeymaker/exchanges/coinbase.rb +69 -0
  38. data/lib/honeymaker/exchanges/gemini.rb +134 -0
  39. data/lib/honeymaker/exchanges/hyperliquid.rb +67 -0
  40. data/lib/honeymaker/exchanges/kraken.rb +122 -0
  41. data/lib/honeymaker/exchanges/kucoin.rb +52 -0
  42. data/lib/honeymaker/exchanges/mexc.rb +56 -0
  43. data/lib/honeymaker/native/kraken.rb +263 -0
  44. data/lib/honeymaker/native.rb +42 -0
  45. data/lib/honeymaker/result.rb +46 -0
  46. data/lib/honeymaker/utils.rb +39 -0
  47. data/lib/honeymaker/version.rb +5 -0
  48. data/lib/honeymaker.rb +97 -0
  49. data/script/parity/kraken_client_live.rb +191 -0
  50. data/script/parity/kraken_live.rb +70 -0
  51. data/script/parity/kraken_transport.rb +604 -0
  52. data/script/parity/resolve.c +49 -0
  53. data/script/release/push_gems.sh +28 -0
  54. data/script/vectors/kraken.rb +50 -0
  55. data/script/vectors/kraken_stage2.rb +146 -0
  56. data/sig/honeymaker.rbs +4 -0
  57. data/test/fixtures/binance_book_ticker.json +7 -0
  58. data/test/fixtures/binance_exchange_info.json +30 -0
  59. data/test/fixtures/bingx_symbols.json +26 -0
  60. data/test/fixtures/bingx_ticker.json +12 -0
  61. data/test/fixtures/bitget_symbols.json +28 -0
  62. data/test/fixtures/bitget_tickers.json +14 -0
  63. data/test/fixtures/bitmart_symbols.json +26 -0
  64. data/test/fixtures/bitmart_ticker.json +12 -0
  65. data/test/fixtures/bitrue_book_ticker.json +7 -0
  66. data/test/fixtures/bitrue_exchange_info.json +34 -0
  67. data/test/fixtures/bitvavo_book_ticker.json +7 -0
  68. data/test/fixtures/bitvavo_markets.json +30 -0
  69. data/test/fixtures/bybit_instruments.json +23 -0
  70. data/test/fixtures/bybit_tickers.json +19 -0
  71. data/test/fixtures/coinbase_product.json +7 -0
  72. data/test/fixtures/coinbase_products.json +24 -0
  73. data/test/fixtures/gemini_pubticker.json +10 -0
  74. data/test/fixtures/gemini_symbol_detail.json +10 -0
  75. data/test/fixtures/gemini_symbols.json +1 -0
  76. data/test/fixtures/hyperliquid_all_mids.json +5 -0
  77. data/test/fixtures/hyperliquid_spot_meta.json +12 -0
  78. data/test/fixtures/kraken_asset_pairs.json +43 -0
  79. data/test/fixtures/kraken_ticker.json +16 -0
  80. data/test/fixtures/kucoin_orderbook_level1.json +10 -0
  81. data/test/fixtures/kucoin_symbols.json +17 -0
  82. data/test/fixtures/mexc_book_ticker.json +7 -0
  83. data/test/fixtures/mexc_exchange_info.json +64 -0
  84. data/test/honeymaker/client_test.rb +90 -0
  85. data/test/honeymaker/clients/binance_client_test.rb +151 -0
  86. data/test/honeymaker/clients/binance_us_client_test.rb +25 -0
  87. data/test/honeymaker/clients/bingx_client_test.rb +106 -0
  88. data/test/honeymaker/clients/bitget_client_test.rb +146 -0
  89. data/test/honeymaker/clients/bitrue_client_test.rb +69 -0
  90. data/test/honeymaker/clients/bitvavo_client_test.rb +156 -0
  91. data/test/honeymaker/clients/bybit_client_test.rb +137 -0
  92. data/test/honeymaker/clients/coinbase_client_test.rb +153 -0
  93. data/test/honeymaker/clients/gemini_client_test.rb +100 -0
  94. data/test/honeymaker/clients/honeymaker_client_registry_test.rb +39 -0
  95. data/test/honeymaker/clients/hyperliquid_client_test.rb +292 -0
  96. data/test/honeymaker/clients/kraken_client_test.rb +255 -0
  97. data/test/honeymaker/clients/kraken_futures_client_test.rb +54 -0
  98. data/test/honeymaker/clients/kucoin_client_test.rb +149 -0
  99. data/test/honeymaker/clients/mexc_client_test.rb +98 -0
  100. data/test/honeymaker/clients/validation_test.rb +167 -0
  101. data/test/honeymaker/error_chain_test.rb +133 -0
  102. data/test/honeymaker/exchange_test.rb +138 -0
  103. data/test/honeymaker/exchanges/binance_test.rb +83 -0
  104. data/test/honeymaker/exchanges/binance_us_test.rb +53 -0
  105. data/test/honeymaker/exchanges/bingx_test.rb +65 -0
  106. data/test/honeymaker/exchanges/bitget_test.rb +65 -0
  107. data/test/honeymaker/exchanges/bitrue_test.rb +65 -0
  108. data/test/honeymaker/exchanges/bitvavo_test.rb +65 -0
  109. data/test/honeymaker/exchanges/bybit_test.rb +55 -0
  110. data/test/honeymaker/exchanges/coinbase_test.rb +106 -0
  111. data/test/honeymaker/exchanges/gemini_catalogue_test.rb +118 -0
  112. data/test/honeymaker/exchanges/gemini_test.rb +184 -0
  113. data/test/honeymaker/exchanges/hyperliquid_test.rb +64 -0
  114. data/test/honeymaker/exchanges/kraken_test.rb +191 -0
  115. data/test/honeymaker/exchanges/kucoin_test.rb +55 -0
  116. data/test/honeymaker/exchanges/mexc_test.rb +97 -0
  117. data/test/honeymaker/honeymaker_test.rb +27 -0
  118. data/test/honeymaker/result_test.rb +42 -0
  119. data/test/honeymaker/utils_test.rb +64 -0
  120. data/test/native/backend_switch_test.rb +53 -0
  121. data/test/native/convert_test.rb +137 -0
  122. data/test/native/gem_isolation_test.rb +17 -0
  123. data/test/native/loader_test.rb +29 -0
  124. data/test/native/lockstep_test.rb +23 -0
  125. data/test/native/signing_smoke_test.rb +28 -0
  126. data/test/parity/kraken_client_parity_test.rb +398 -0
  127. data/test/parity/kraken_exchange_parity_test.rb +172 -0
  128. data/test/release/branch_hardening_test.rb +34 -0
  129. data/test/release/push_gems_test.rb +79 -0
  130. data/test/smoke.rb +24 -0
  131. data/test/support/fixture_helper.rb +13 -0
  132. data/test/support/kraken_signing_smoke.rb +21 -0
  133. data/test/support/parity_helper.rb +62 -0
  134. data/test/support/venue_double.rb +100 -0
  135. data/test/test_helper.rb +9 -0
  136. metadata +254 -0
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Exchanges::BinanceTest < Minitest::Test
6
+ include FixtureHelper
7
+
8
+ def setup
9
+ @exchange = Honeymaker::Exchanges::Binance.new
10
+ end
11
+
12
+ def test_get_tickers_info_parses_response
13
+ body = load_fixture("binance_exchange_info.json")
14
+ stub_connection(body)
15
+
16
+ result = @exchange.get_tickers_info
17
+
18
+ assert result.success?
19
+ ticker = result.data.first
20
+ assert_equal "BTCUSDT", ticker[:ticker]
21
+ assert_equal "BTC", ticker[:base]
22
+ assert_equal "USDT", ticker[:quote]
23
+ assert_equal "0.00000100", ticker[:minimum_base_size]
24
+ assert_equal "5.00000000", ticker[:minimum_quote_size]
25
+ assert_equal 6, ticker[:base_decimals]
26
+ assert_equal 8, ticker[:quote_decimals]
27
+ assert_equal 2, ticker[:price_decimals]
28
+ assert ticker[:available]
29
+ assert ticker[:trading_enabled]
30
+ end
31
+
32
+ def test_get_tickers_info_marks_non_trading_pairs_disabled
33
+ body = load_fixture("binance_exchange_info.json")
34
+ body["symbols"].first["status"] = "BREAK"
35
+ stub_connection(body)
36
+
37
+ result = @exchange.get_tickers_info
38
+
39
+ ticker = result.data.first
40
+ assert ticker[:available] # still listed
41
+ refute ticker[:trading_enabled] # but not trading
42
+ end
43
+
44
+ def test_get_tickers_info_handles_api_error
45
+ connection = stub
46
+ connection.stubs(:get).raises(Faraday::ServerError.new("500", { status: 500, body: "Internal Server Error" }))
47
+ @exchange.instance_variable_set(:@connection, connection)
48
+
49
+ result = @exchange.get_tickers_info
50
+
51
+ assert result.failure?
52
+ end
53
+
54
+ def test_get_bid_ask_parses_response
55
+ body = load_fixture("binance_book_ticker.json")
56
+ stub_connection(body)
57
+
58
+ result = @exchange.get_bid_ask("BTCUSDT")
59
+
60
+ assert result.success?
61
+ assert_equal BigDecimal("67123.45"), result.data[:bid]
62
+ assert_equal BigDecimal("67125.67"), result.data[:ask]
63
+ end
64
+
65
+ def test_get_bid_ask_handles_api_error
66
+ connection = stub
67
+ connection.stubs(:get).raises(Faraday::ServerError.new("500", { status: 500, body: "Internal Server Error" }))
68
+ @exchange.instance_variable_set(:@connection, connection)
69
+
70
+ result = @exchange.get_bid_ask("BTCUSDT")
71
+
72
+ assert result.failure?
73
+ end
74
+
75
+ private
76
+
77
+ def stub_connection(body)
78
+ response = stub(body: body)
79
+ connection = stub
80
+ connection.stubs(:get).with { |path, &block| block&.call(OpenStruct.new(params: {})); true }.returns(response)
81
+ @exchange.instance_variable_set(:@connection, connection)
82
+ end
83
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Exchanges::BinanceUsTest < Minitest::Test
6
+ include FixtureHelper
7
+
8
+ def setup
9
+ @exchange = Honeymaker::Exchanges::BinanceUs.new
10
+ end
11
+
12
+ def test_inherits_from_binance
13
+ assert_kind_of Honeymaker::Exchanges::Binance, @exchange
14
+ end
15
+
16
+ def test_uses_binance_us_url
17
+ assert_equal "https://api.binance.us", Honeymaker::Exchanges::BinanceUs::BASE_URL
18
+ end
19
+
20
+ def test_get_tickers_info_parses_response
21
+ body = load_fixture("binance_exchange_info.json")
22
+ stub_connection(body)
23
+
24
+ result = @exchange.get_tickers_info
25
+
26
+ assert result.success?
27
+ ticker = result.data.first
28
+ assert_equal "BTCUSDT", ticker[:ticker]
29
+ assert_equal "BTC", ticker[:base]
30
+ assert ticker[:available]
31
+ assert ticker[:trading_enabled]
32
+ end
33
+
34
+ def test_get_bid_ask_parses_response
35
+ body = load_fixture("binance_book_ticker.json")
36
+ stub_connection(body)
37
+
38
+ result = @exchange.get_bid_ask("BTCUSDT")
39
+
40
+ assert result.success?
41
+ assert_equal BigDecimal("67123.45"), result.data[:bid]
42
+ assert_equal BigDecimal("67125.67"), result.data[:ask]
43
+ end
44
+
45
+ private
46
+
47
+ def stub_connection(body)
48
+ response = stub(body: body)
49
+ connection = stub
50
+ connection.stubs(:get).with { |_, &block| block&.call(OpenStruct.new(params: {})); true }.returns(response)
51
+ @exchange.instance_variable_set(:@connection, connection)
52
+ end
53
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Exchanges::BingXTest < Minitest::Test
6
+ include FixtureHelper
7
+
8
+ def setup
9
+ @exchange = Honeymaker::Exchanges::BingX.new
10
+ end
11
+
12
+ def test_get_tickers_info_parses_response
13
+ body = load_fixture("bingx_symbols.json")
14
+ stub_connection(body)
15
+
16
+ result = @exchange.get_tickers_info
17
+
18
+ assert result.success?
19
+ # "INVALID" has no dash so it's skipped
20
+ assert_equal 1, result.data.size
21
+
22
+ ticker = result.data.first
23
+ assert_equal "BTC-USDT", ticker[:ticker]
24
+ assert_equal "BTC", ticker[:base]
25
+ assert_equal "USDT", ticker[:quote]
26
+ assert_equal "0.00001", ticker[:minimum_base_size]
27
+ assert_equal "1", ticker[:minimum_quote_size]
28
+ assert_equal "500", ticker[:maximum_base_size]
29
+ assert_equal 5, 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_skips_symbols_without_dash
37
+ body = load_fixture("bingx_symbols.json")
38
+ stub_connection(body)
39
+
40
+ result = @exchange.get_tickers_info
41
+
42
+ tickers = result.data.map { |t| t[:ticker] }
43
+ refute_includes tickers, "INVALID"
44
+ end
45
+
46
+ def test_get_bid_ask_parses_response
47
+ body = load_fixture("bingx_ticker.json")
48
+ stub_connection(body)
49
+
50
+ result = @exchange.get_bid_ask("BTC-USDT")
51
+
52
+ assert result.success?
53
+ assert_equal BigDecimal("67123.45"), result.data[:bid]
54
+ assert_equal BigDecimal("67125.67"), result.data[:ask]
55
+ end
56
+
57
+ private
58
+
59
+ def stub_connection(body)
60
+ response = stub(body: body)
61
+ connection = stub
62
+ connection.stubs(:get).returns(response)
63
+ @exchange.instance_variable_set(:@connection, connection)
64
+ end
65
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Exchanges::BitgetTest < Minitest::Test
6
+ include FixtureHelper
7
+
8
+ def setup
9
+ @exchange = Honeymaker::Exchanges::Bitget.new
10
+ end
11
+
12
+ def test_get_tickers_info_parses_response
13
+ body = load_fixture("bitget_symbols.json")
14
+ stub_connection(body)
15
+
16
+ result = @exchange.get_tickers_info
17
+
18
+ assert result.success?
19
+ assert_equal 2, result.data.size
20
+
21
+ ticker = result.data.first
22
+ assert_equal "BTCUSDT", 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 "5", ticker[:minimum_quote_size]
27
+ assert_equal "9000", ticker[:maximum_base_size]
28
+ assert_equal 6, ticker[:base_decimals]
29
+ assert_equal 8, ticker[:quote_decimals]
30
+ assert_equal 2, ticker[:price_decimals]
31
+ assert ticker[:available]
32
+ assert ticker[:trading_enabled]
33
+ end
34
+
35
+ def test_offline_symbol_listed_but_not_trading_enabled
36
+ body = load_fixture("bitget_symbols.json")
37
+ stub_connection(body)
38
+
39
+ result = @exchange.get_tickers_info
40
+
41
+ eth = result.data.find { |t| t[:ticker] == "ETHUSDT" }
42
+ assert eth[:available] # still listed
43
+ refute eth[:trading_enabled] # but not trading
44
+ end
45
+
46
+ def test_get_bid_ask_parses_response
47
+ body = load_fixture("bitget_tickers.json")
48
+ stub_connection(body)
49
+
50
+ result = @exchange.get_bid_ask("BTCUSDT")
51
+
52
+ assert result.success?
53
+ assert_equal BigDecimal("67123.45"), result.data[:bid]
54
+ assert_equal BigDecimal("67125.67"), result.data[:ask]
55
+ end
56
+
57
+ private
58
+
59
+ def stub_connection(body)
60
+ response = stub(body: body)
61
+ connection = stub
62
+ connection.stubs(:get).returns(response)
63
+ @exchange.instance_variable_set(:@connection, connection)
64
+ end
65
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Exchanges::BitrueTest < Minitest::Test
6
+ include FixtureHelper
7
+
8
+ def setup
9
+ @exchange = Honeymaker::Exchanges::Bitrue.new
10
+ end
11
+
12
+ def test_get_tickers_info_parses_response
13
+ body = load_fixture("bitrue_exchange_info.json")
14
+ stub_connection(body)
15
+
16
+ result = @exchange.get_tickers_info
17
+
18
+ assert result.success?
19
+ assert_equal 2, result.data.size
20
+
21
+ ticker = result.data.first
22
+ assert_equal "BTCUSDT", 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 "5", ticker[:minimum_quote_size]
27
+ assert_equal "9000", ticker[:maximum_base_size]
28
+ assert_equal 5, ticker[:base_decimals]
29
+ assert_equal 2, ticker[:quote_decimals]
30
+ assert_equal 2, ticker[:price_decimals]
31
+ assert ticker[:available]
32
+ assert ticker[:trading_enabled]
33
+ end
34
+
35
+ def test_falls_back_to_precision_fields_when_no_filters
36
+ body = load_fixture("bitrue_exchange_info.json")
37
+ stub_connection(body)
38
+
39
+ result = @exchange.get_tickers_info
40
+
41
+ eth = result.data.find { |t| t[:ticker] == "ETHUSDT" }
42
+ assert_equal 6, eth[:base_decimals]
43
+ assert_equal 2, eth[:price_decimals]
44
+ end
45
+
46
+ def test_get_bid_ask_parses_response
47
+ body = load_fixture("bitrue_book_ticker.json")
48
+ stub_connection(body)
49
+
50
+ result = @exchange.get_bid_ask("BTCUSDT")
51
+
52
+ assert result.success?
53
+ assert_equal BigDecimal("67123.45"), result.data[:bid]
54
+ assert_equal BigDecimal("67125.67"), result.data[:ask]
55
+ end
56
+
57
+ private
58
+
59
+ def stub_connection(body)
60
+ response = stub(body: body)
61
+ connection = stub
62
+ connection.stubs(:get).returns(response)
63
+ @exchange.instance_variable_set(:@connection, connection)
64
+ end
65
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Exchanges::BitvavoTest < Minitest::Test
6
+ include FixtureHelper
7
+
8
+ def setup
9
+ @exchange = Honeymaker::Exchanges::Bitvavo.new
10
+ end
11
+
12
+ def test_get_tickers_info_parses_response
13
+ body = load_fixture("bitvavo_markets.json")
14
+ stub_connection(body)
15
+
16
+ result = @exchange.get_tickers_info
17
+
18
+ assert result.success?
19
+ assert_equal 2, result.data.size
20
+
21
+ ticker = result.data.first
22
+ assert_equal "BTC-EUR", ticker[:ticker]
23
+ assert_equal "BTC", ticker[:base]
24
+ assert_equal "EUR", ticker[:quote]
25
+ assert_equal "0.00001", ticker[:minimum_base_size]
26
+ assert_equal "5", ticker[:minimum_quote_size]
27
+ assert_nil ticker[:maximum_base_size]
28
+ assert_equal 8, ticker[:base_decimals]
29
+ assert_equal 2, ticker[:quote_decimals]
30
+ assert_equal 0, ticker[:price_decimals] # tickSize "1.00" -> whole-number prices
31
+ assert ticker[:available]
32
+ assert ticker[:trading_enabled]
33
+ end
34
+
35
+ def test_halted_market_listed_but_not_trading_enabled
36
+ body = load_fixture("bitvavo_markets.json")
37
+ stub_connection(body)
38
+
39
+ result = @exchange.get_tickers_info
40
+
41
+ eth = result.data.find { |t| t[:ticker] == "ETH-EUR" }
42
+ assert eth[:available] # still listed
43
+ refute eth[:trading_enabled] # but not trading
44
+ end
45
+
46
+ def test_get_bid_ask_parses_response
47
+ body = load_fixture("bitvavo_book_ticker.json")
48
+ stub_connection(body)
49
+
50
+ result = @exchange.get_bid_ask("BTC-EUR")
51
+
52
+ assert result.success?
53
+ assert_equal BigDecimal("67123.45"), result.data[:bid]
54
+ assert_equal BigDecimal("67125.67"), result.data[:ask]
55
+ end
56
+
57
+ private
58
+
59
+ def stub_connection(body)
60
+ response = stub(body: body)
61
+ connection = stub
62
+ connection.stubs(:get).returns(response)
63
+ @exchange.instance_variable_set(:@connection, connection)
64
+ end
65
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Exchanges::BybitTest < Minitest::Test
6
+ include FixtureHelper
7
+
8
+ def setup
9
+ @exchange = Honeymaker::Exchanges::Bybit.new
10
+ end
11
+
12
+ def test_get_tickers_info_parses_response
13
+ body = load_fixture("bybit_instruments.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 "BTCUSDT", ticker[:ticker]
23
+ assert_equal "BTC", ticker[:base]
24
+ assert_equal "USDT", ticker[:quote]
25
+ assert_equal "0.000048", ticker[:minimum_base_size]
26
+ assert_equal "1", ticker[:minimum_quote_size]
27
+ assert_equal "71.73956243", ticker[:maximum_base_size]
28
+ assert_equal "4000000", ticker[:maximum_quote_size]
29
+ assert_equal 6, ticker[:base_decimals]
30
+ assert_equal 8, 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_bid_ask_parses_response
37
+ body = load_fixture("bybit_tickers.json")
38
+ stub_connection(body)
39
+
40
+ result = @exchange.get_bid_ask("BTCUSDT")
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).with { |_, &block| block&.call(OpenStruct.new(params: {})); true }.returns(response)
53
+ @exchange.instance_variable_set(:@connection, connection)
54
+ end
55
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Exchanges::CoinbaseTest < Minitest::Test
6
+ include FixtureHelper
7
+
8
+ def setup
9
+ @exchange = Honeymaker::Exchanges::Coinbase.new
10
+ end
11
+
12
+ def test_get_tickers_info_parses_response
13
+ body = load_fixture("coinbase_products.json")
14
+ stub_connection(body)
15
+
16
+ result = @exchange.get_tickers_info
17
+
18
+ assert result.success?
19
+ # RNDR is blacklisted, so only BTC-USD should remain
20
+ assert_equal 1, result.data.size
21
+ ticker = result.data.first
22
+ assert_equal "BTC-USD", ticker[:ticker]
23
+ assert_equal "BTC", ticker[:base]
24
+ assert_equal "USD", ticker[:quote]
25
+ assert_equal "0.00000001", ticker[:minimum_base_size]
26
+ assert_equal "1", ticker[:minimum_quote_size]
27
+ assert_equal "3400", ticker[:maximum_base_size]
28
+ assert_equal 8, ticker[:base_decimals]
29
+ assert_equal 2, ticker[:quote_decimals]
30
+ assert_equal 2, ticker[:price_decimals]
31
+ assert ticker[:available]
32
+ assert ticker[:trading_enabled] # no disabled signal -> defaults to true
33
+ end
34
+
35
+ def test_get_tickers_info_disables_when_trading_disabled
36
+ body = load_fixture("coinbase_products.json")
37
+ body["products"].first["trading_disabled"] = true
38
+ stub_connection(body)
39
+
40
+ result = @exchange.get_tickers_info
41
+
42
+ ticker = result.data.first
43
+ assert ticker[:available] # still listed
44
+ refute ticker[:trading_enabled] # but not trading
45
+ end
46
+
47
+ def test_get_tickers_info_disables_when_status_not_online
48
+ body = load_fixture("coinbase_products.json")
49
+ body["products"].first["status"] = "delisted"
50
+ stub_connection(body)
51
+
52
+ result = @exchange.get_tickers_info
53
+
54
+ refute result.data.first[:trading_enabled]
55
+ end
56
+
57
+ def test_get_tickers_info_disables_restricted_modes
58
+ %w[limit_only cancel_only is_disabled].each do |flag|
59
+ body = load_fixture("coinbase_products.json")
60
+ body["products"].first[flag] = true
61
+ stub_connection(body)
62
+
63
+ ticker = @exchange.get_tickers_info.data.first
64
+ assert ticker[:available], flag
65
+ refute ticker[:trading_enabled], flag
66
+ end
67
+ end
68
+
69
+ def test_keeps_render
70
+ body = load_fixture("coinbase_products.json")
71
+ body["products"] << body["products"].last.merge("product_id" => "RENDER-USD")
72
+ stub_connection(body)
73
+
74
+ assert_includes @exchange.get_tickers_info.data.map { |t| t[:base] }, "RENDER"
75
+ end
76
+
77
+ def test_filters_blacklisted_assets
78
+ body = load_fixture("coinbase_products.json")
79
+ stub_connection(body)
80
+
81
+ result = @exchange.get_tickers_info
82
+
83
+ bases = result.data.map { |t| t[:base] }
84
+ refute_includes bases, "RNDR"
85
+ end
86
+
87
+ def test_get_bid_ask_parses_response
88
+ body = load_fixture("coinbase_product.json")
89
+ stub_connection(body)
90
+
91
+ result = @exchange.get_bid_ask("BTC-USD")
92
+
93
+ assert result.success?
94
+ assert_equal BigDecimal("67123.45"), result.data[:bid]
95
+ assert_equal BigDecimal("67125.67"), result.data[:ask]
96
+ end
97
+
98
+ private
99
+
100
+ def stub_connection(body)
101
+ response = stub(body: body)
102
+ connection = stub
103
+ connection.stubs(:get).returns(response)
104
+ @exchange.instance_variable_set(:@connection, connection)
105
+ end
106
+ end
@@ -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