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.
- 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,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
|