honeymaker 0.11.1 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0d4a030c628dda5a597efb6ff65b552ce961cfb855e7f6affa668a8f0e6f492
4
- data.tar.gz: 43372bdcc5a896dcf83e7f8fcd913d091a1b2c79b61e03e2b1e430dc20821ee0
3
+ metadata.gz: 2367ab9c6994586eb565f383f4b897d7a7bdcfb5cf68c0593da80f6b4d4b0497
4
+ data.tar.gz: 585589c4bc524266bed905b142913b6072e5ff8824602cdf5691e6ac2046bf39
5
5
  SHA512:
6
- metadata.gz: aabd7c7f6859af5e36fb7bc301a227603683b9505e78a306b3944b8fba8ed6c5df45c05a9846557b18c59f461bfe3601f7492ede5a02c6125a87a1d9d58e76fe
7
- data.tar.gz: ef8db9f4be0d5d60aa65a0d9baa41283b20cf44ddf6f624b4c0617cc5fab6449ac77878b190d0dff2ebca848876461a500b95d6b53f39f9b465fa817567ad923
6
+ metadata.gz: b4d5824b776181fff4494c1f4fe76e8ff46e21faf198cb23a894ea836fcfb4bc4f3eb8c0eec2315cc072a065ce9de5b4fd9e34d50f52390368069c5fbd091b13
7
+ data.tar.gz: f63b8235706bc5257bc1710939fc68f315316650966476a55ff4a25c1de35bb3035f42f54a94105fd1e446bcb4c6e684b65b2418bb16db68f7310c5bdb17dd16
@@ -65,9 +65,13 @@ module Honeymaker
65
65
  post_private("/0/private/CancelOrder", { nonce: nonce, txid: txid, cl_ord_id: cl_ord_id })
66
66
  end
67
67
 
68
- def get_tradable_asset_pairs(pairs: nil, info: nil, country_code: nil)
68
+ # aclass_base: "all" also returns tokenized equities (xStocks); omitted, Kraken serves only the
69
+ # "currency" class. Note the asset-class parameter is spelled differently per endpoint —
70
+ # aclass_base here, asset_class on Ticker/OHLC/Depth/AddOrder.
71
+ def get_tradable_asset_pairs(pairs: nil, info: nil, country_code: nil, aclass_base: nil)
69
72
  get_public("/0/public/AssetPairs", {
70
- pair: pairs ? pairs.join(",") : nil, info: info, country_code: country_code
73
+ pair: pairs ? pairs.join(",") : nil, info: info, country_code: country_code,
74
+ aclass_base: aclass_base
71
75
  })
72
76
  end
73
77
 
@@ -48,15 +48,25 @@ module Honeymaker
48
48
 
49
49
  def get_tickers_info
50
50
  with_rescue do
51
- response = connection.get("/0/public/AssetPairs")
51
+ # aclass_base=all, not a bare call: the default response carries only the "currency" class,
52
+ # so Kraken tokenized equities (xStocks) are invisible without it. One request returns both
53
+ # classes and they are disjoint.
54
+ response = connection.get("/0/public/AssetPairs", { aclass_base: "all" })
52
55
 
53
56
  error = response.body["error"]
54
57
  return Result::Failure.new(*error) if error.is_a?(Array) && error.any?
55
58
 
56
- response.body["result"].filter_map do |_, info|
59
+ # Every tokenized pair is returned TWICE - once under an SPV key (NVDASPVUSD), once under
60
+ # the x key (NVDAxUSD) - sharing one wsname and altname. Currency pairs are never
61
+ # duplicated, so keying by wsname collapses exactly the aliases and nothing else.
62
+ deduped = response.body["result"].each_with_object({}) do |(_, info), acc|
57
63
  wsname = info["wsname"]
58
- next unless wsname && !wsname.empty?
64
+ next if wsname.nil? || wsname.empty?
59
65
 
66
+ acc[wsname] ||= info
67
+ end
68
+
69
+ deduped.filter_map do |wsname, info|
60
70
  base, quote = wsname.split("/")
61
71
 
62
72
  {
@@ -71,7 +81,15 @@ module Honeymaker
71
81
  quote_decimals: info["cost_decimals"],
72
82
  price_decimals: info["pair_decimals"],
73
83
  available: true,
74
- trading_enabled: info.key?("status") ? info["status"] == "online" : true
84
+ # Tokenized equities are listed but never tradable through this client: AddOrder needs
85
+ # an asset_class parameter it does not send, and Kraken closes the tokenized order
86
+ # books to EEA clients over the API regardless of that. Listing them anyway is what
87
+ # lets a holding be resolved and valued.
88
+ trading_enabled: if info["aclass_base"] == "tokenized_asset"
89
+ false
90
+ else
91
+ info.key?("status") ? info["status"] == "online" : true
92
+ end
75
93
  }
76
94
  end
77
95
  end
@@ -24,7 +24,10 @@ module Honeymaker
24
24
  quote_decimals: product["quoteAssetPrecision"],
25
25
  price_decimals: f[:price] ? Utils.decimals(f[:price]["tickSize"]) : product["quotePrecision"],
26
26
  available: true,
27
- trading_enabled: product["status"] == "TRADING"
27
+ # MEXC encodes status as "1", not Binance's "TRADING" — every symbol it returns is "1".
28
+ # isSpotTradingAllowed is the real gate: ~5% of listed symbols reject a spot order.
29
+ # `!= false` keeps the fail-open default if the key is ever absent, matching the siblings.
30
+ trading_enabled: product["status"] == "1" && product["isSpotTradingAllowed"] != false
28
31
  }
29
32
  end
30
33
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Honeymaker
4
- VERSION = "0.11.1"
4
+ VERSION = "0.11.4"
5
5
  end
@@ -10,7 +10,34 @@
10
10
  "cost_decimals": 5,
11
11
  "pair_decimals": 1,
12
12
  "ordermin": "0.00010000",
13
- "costmin": "0.50000"
13
+ "costmin": "0.50000",
14
+ "aclass_base": "currency"
15
+ },
16
+ "NVDAxUSD": {
17
+ "altname": "NVDAxUSD",
18
+ "wsname": "NVDAx/USD",
19
+ "aclass_base": "tokenized_asset",
20
+ "base": "NVDAx",
21
+ "quote": "ZUSD",
22
+ "lot_decimals": 8,
23
+ "cost_decimals": 5,
24
+ "pair_decimals": 2,
25
+ "ordermin": "0.00000001",
26
+ "costmin": "0.5",
27
+ "status": "online"
28
+ },
29
+ "NVDASPVUSD": {
30
+ "altname": "NVDAxUSD",
31
+ "wsname": "NVDAx/USD",
32
+ "aclass_base": "tokenized_asset",
33
+ "base": "NVDAx",
34
+ "quote": "ZUSD",
35
+ "lot_decimals": 8,
36
+ "cost_decimals": 5,
37
+ "pair_decimals": 2,
38
+ "ordermin": "0.00000001",
39
+ "costmin": "0.5",
40
+ "status": "online"
14
41
  }
15
42
  }
16
43
  }
@@ -1,40 +1,64 @@
1
1
  {
2
+ "_provenance": "Real capture: GET https://api.mexc.com/api/v3/exchangeInfo (2026-09-05), trimmed to two symbols and to the fields this parser reads. MEXC returns status \"1\" for every symbol and PERCENT_PRICE_BY_SIDE as its only filter \u2014 it emits neither \"TRADING\" nor LOT_SIZE/PRICE_FILTER/MIN_NOTIONAL. Re-capture and diff rather than hand-editing; mexc_test.rb asserts this file's SHA-256.",
3
+ "timezone": "CST",
2
4
  "symbols": [
3
5
  {
4
- "symbol": "BTCUSDT",
5
- "status": "TRADING",
6
- "baseAsset": "BTC",
6
+ "symbol": "METALUSDT",
7
+ "status": "1",
8
+ "baseAsset": "METAL",
9
+ "baseAssetPrecision": 2,
7
10
  "quoteAsset": "USDT",
8
- "baseAssetPrecision": 8,
9
- "quoteAssetPrecision": 8,
10
- "quotePrecision": 2,
11
+ "quotePrecision": 5,
12
+ "quoteAssetPrecision": 5,
13
+ "orderTypes": [
14
+ "LIMIT",
15
+ "MARKET",
16
+ "LIMIT_MAKER"
17
+ ],
18
+ "isSpotTradingAllowed": true,
19
+ "isMarginTradingAllowed": false,
20
+ "quoteAmountPrecision": "1",
21
+ "baseSizePrecision": "0.1",
22
+ "permissions": [
23
+ "SPOT"
24
+ ],
11
25
  "filters": [
12
26
  {
13
- "filterType": "PRICE_FILTER",
14
- "tickSize": "0.01"
15
- },
16
- {
17
- "filterType": "LOT_SIZE",
18
- "minQty": "0.00001",
19
- "maxQty": "9000",
20
- "stepSize": "0.00001"
21
- },
22
- {
23
- "filterType": "MIN_NOTIONAL",
24
- "minNotional": "5",
25
- "maxNotional": "9000000"
27
+ "filterType": "PERCENT_PRICE_BY_SIDE",
28
+ "bidMultiplierUp": "0.2",
29
+ "askMultiplierDown": "0.2"
26
30
  }
27
- ]
31
+ ],
32
+ "maxQuoteAmount": "2000000"
28
33
  },
29
34
  {
30
- "symbol": "ETHUSDT",
31
- "status": "HALT",
32
- "baseAsset": "ETH",
35
+ "symbol": "PALMAIUSDT",
36
+ "status": "1",
37
+ "baseAsset": "PALMAI",
38
+ "baseAssetPrecision": 2,
33
39
  "quoteAsset": "USDT",
34
- "baseAssetPrecision": 6,
35
- "quoteAssetPrecision": 8,
36
- "quotePrecision": 2,
37
- "filters": []
40
+ "quotePrecision": 4,
41
+ "quoteAssetPrecision": 4,
42
+ "orderTypes": [
43
+ "LIMIT",
44
+ "MARKET",
45
+ "LIMIT_MAKER"
46
+ ],
47
+ "isSpotTradingAllowed": false,
48
+ "isMarginTradingAllowed": false,
49
+ "quoteAmountPrecision": "1",
50
+ "baseSizePrecision": "0",
51
+ "permissions": [
52
+ "SPOT"
53
+ ],
54
+ "filters": [
55
+ {
56
+ "filterType": "PERCENT_PRICE_BY_SIDE",
57
+ "bidMultiplierUp": "0.2",
58
+ "askMultiplierDown": "0.2"
59
+ }
60
+ ],
61
+ "maxQuoteAmount": "2000000"
38
62
  }
39
63
  ]
40
64
  }
@@ -22,6 +22,36 @@ class Honeymaker::Clients::KrakenTest < Minitest::Test
22
22
  assert result.data["result"].key?("XBTUSDT")
23
23
  end
24
24
 
25
+ # Without aclass_base Kraken serves only the "currency" class, so tokenized equities are invisible.
26
+ def test_get_tradable_asset_pairs_forwards_aclass_base
27
+ captured = nil
28
+ req = stub("req")
29
+ req.stubs(:url)
30
+ req.stubs(:headers=)
31
+ req.stubs(:params=).with { |p| captured = p; true }
32
+ connection = stub
33
+ connection.stubs(:get).yields(req).returns(stub(body: { "error" => [], "result" => {} }))
34
+ @client.instance_variable_set(:@connection, connection)
35
+
36
+ assert @client.get_tradable_asset_pairs(aclass_base: "all").success?
37
+ assert_equal "all", captured[:aclass_base]
38
+ end
39
+
40
+ def test_get_tradable_asset_pairs_omits_aclass_base_when_not_asked
41
+ captured = nil
42
+ req = stub("req")
43
+ req.stubs(:url)
44
+ req.stubs(:headers=)
45
+ req.stubs(:params=).with { |p| captured = p; true }
46
+ connection = stub
47
+ connection.stubs(:get).yields(req).returns(stub(body: { "error" => [], "result" => {} }))
48
+ @client.instance_variable_set(:@connection, connection)
49
+
50
+ @client.get_tradable_asset_pairs
51
+
52
+ refute captured.key?(:aclass_base), "params are compacted, so nil must not be sent"
53
+ end
54
+
25
55
  def test_get_ticker_information
26
56
  stub_connection(:get, { "error" => [], "result" => { "XBTUSDT" => { "a" => ["50000"] } } })
27
57
  result = @client.get_ticker_information(pair: "XBTUSDT")
@@ -9,6 +9,45 @@ class Honeymaker::Exchanges::KrakenTest < Minitest::Test
9
9
  @exchange = Honeymaker::Exchanges::Kraken.new
10
10
  end
11
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
+
12
51
  def test_get_tickers_info_parses_response
13
52
  body = load_fixture("kraken_asset_pairs.json")
14
53
  stub_request(body)
@@ -68,7 +107,9 @@ class Honeymaker::Exchanges::KrakenTest < Minitest::Test
68
107
  result = @exchange.get_tickers_info
69
108
 
70
109
  assert result.success?
71
- assert_empty result.data
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" }
72
113
  end
73
114
 
74
115
  def test_get_tickers_info_uses_real_costmin
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "test_helper"
4
+ require "digest"
4
5
 
5
6
  class Honeymaker::Exchanges::MexcTest < Minitest::Test
6
7
  include FixtureHelper
@@ -9,6 +10,23 @@ class Honeymaker::Exchanges::MexcTest < Minitest::Test
9
10
  @exchange = Honeymaker::Exchanges::Mexc.new
10
11
  end
11
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
+
12
30
  def test_get_tickers_info_parses_response
13
31
  body = load_fixture("mexc_exchange_info.json")
14
32
  stub_connection(body)
@@ -19,40 +37,42 @@ class Honeymaker::Exchanges::MexcTest < Minitest::Test
19
37
  assert_equal 2, result.data.size
20
38
 
21
39
  ticker = result.data.first
22
- assert_equal "BTCUSDT", ticker[:ticker]
23
- assert_equal "BTC", ticker[:base]
40
+ assert_equal "METALUSDT", ticker[:ticker]
41
+ assert_equal "METAL", ticker[:base]
24
42
  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 8, ticker[:quote_decimals]
30
- assert_equal 2, ticker[:price_decimals]
31
43
  assert ticker[:available]
32
44
  assert ticker[:trading_enabled]
33
45
  end
34
46
 
35
- def test_halted_symbol_listed_but_not_trading_enabled
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
36
51
  body = load_fixture("mexc_exchange_info.json")
37
52
  stub_connection(body)
38
53
 
39
54
  result = @exchange.get_tickers_info
40
55
 
41
- halted = result.data.find { |t| t[:ticker] == "ETHUSDT" }
42
- assert halted[:available] # still listed
43
- refute halted[:trading_enabled] # but not trading
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
44
60
  end
45
61
 
46
- def test_falls_back_to_precision_fields_when_no_filters
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
47
65
  body = load_fixture("mexc_exchange_info.json")
48
66
  stub_connection(body)
49
67
 
50
68
  result = @exchange.get_tickers_info
51
69
 
52
- # ETHUSDT has empty filters, should use baseAssetPrecision/quotePrecision
53
- eth = result.data.find { |t| t[:ticker] == "ETHUSDT" }
54
- assert_equal 6, eth[:base_decimals]
55
- assert_equal 2, eth[:price_decimals]
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]
56
76
  end
57
77
 
58
78
  def test_get_bid_ask_parses_response
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: honeymaker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.11.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Deltabadger