honeymaker 0.12.5-aarch64-linux → 0.12.6-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be39a594078cccd45823dd961e0b538f9b4e4eef2157226d5db7960c8fca4ba2
4
- data.tar.gz: 441eee94d7c3081275fdedea9175c18f4720cb0618390a5b520b15add7381765
3
+ metadata.gz: facaf702a43fc739fe970a71f72017dd240617a8938a53934adb892c968f91cf
4
+ data.tar.gz: f7076b3470b45e3250f5794894f8e7e6a93cbcf034c76a67e117eea52649e5d1
5
5
  SHA512:
6
- metadata.gz: 99c9125cb2b808559d92b7c781cb37ae3dc020c88fe4a7f5ce87904ecac8bb8340572a7649e646e70f983df5acec52d00efd0c36df61df0251909ba4dfebca4e
7
- data.tar.gz: 5712715c2835590793bd94306738b8d4550af4dfe8c1dda20547f28e134bcaa0864420578545da8c797d00f084ef804fde8628b020938b3d1f33151dca2f9f3a
6
+ metadata.gz: 8b19a598c52226ea8b503c9b887e0d091cc387937799475af8b65473907c8ce3e5546126cdaa73f1c0610d6c5bcf36f46f181c190a01b14e9c7f8c85d297a637
7
+ data.tar.gz: 7363a8ca3f95e42abd6dbb6584db23bcb80c6dc2872067e23a930c75d51ff80f737df96a50ec01547051591a4a2725b137d56b5690c0a9f54fe496070180856a
@@ -18,7 +18,9 @@ module Honeymaker
18
18
  ticker: ticker,
19
19
  base: parts[0],
20
20
  quote: parts[1],
21
- minimum_base_size: product["minQty"]&.to_s,
21
+ # minQty is deprecated and far above what BingX accepts (~15 USDT on BTC-USDT against a
22
+ # 0.5 minNotional); the notional is the venue's only real floor.
23
+ minimum_base_size: "0",
22
24
  minimum_quote_size: product["minNotional"]&.to_s,
23
25
  maximum_base_size: product["maxQty"]&.to_s,
24
26
  maximum_quote_size: product["maxNotional"]&.to_s,
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bigdecimal/util"
4
+
3
5
  module Honeymaker
4
6
  module Exchanges
5
7
  class Bitget < Exchange
@@ -8,14 +10,16 @@ module Honeymaker
8
10
  def get_tickers_info
9
11
  with_rescue do
10
12
  response = connection.get("/api/v2/spot/public/symbols")
13
+ products = response.body["data"]
14
+ prices = products.any? { |p| p["quoteCoin"] != "USDT" } ? usdt_prices : {}
11
15
 
12
- response.body["data"].filter_map do |product|
16
+ products.filter_map do |product|
13
17
  {
14
18
  ticker: product["symbol"],
15
19
  base: product["baseCoin"],
16
20
  quote: product["quoteCoin"],
17
21
  minimum_base_size: product["minTradeAmount"],
18
- minimum_quote_size: product["minTradeUSDT"],
22
+ minimum_quote_size: quote_minimum(product["quoteCoin"], product["minTradeUSDT"], prices),
19
23
  maximum_base_size: product["maxTradeAmount"],
20
24
  maximum_quote_size: nil,
21
25
  base_decimals: product["quantityPrecision"].to_i,
@@ -44,6 +48,31 @@ module Honeymaker
44
48
 
45
49
  private
46
50
 
51
+ # minTradeUSDT is a USDT figure on every pair, whatever its quote
52
+ # (https://www.bitget.com/docs/catalog/classic-spot-market/classic-spot-market#get-symbol-info), so
53
+ # on ETHBTC its 1 meant a 1 BTC minimum. Converted at the quote's own USDT price (never the
54
+ # traded pair's); with no price to convert at, the minimum is left to the venue (0) rather than
55
+ # guessed. Same rule as deltabadger's Exchanges::Bitget#quote_minimum.
56
+ def quote_minimum(quote, usdt_minimum, prices)
57
+ minimum = usdt_minimum.to_s.to_d
58
+ return usdt_minimum if quote == "USDT" || minimum.zero?
59
+
60
+ if (price = prices["#{quote}USDT"].to_d).positive?
61
+ (minimum / price).to_s("F")
62
+ elsif (price = prices["USDT#{quote}"].to_d).positive?
63
+ (minimum * price).to_s("F")
64
+ else
65
+ "0"
66
+ end
67
+ end
68
+
69
+ # symbol => lastPr for the whole market; {} when the read fails, so conversions fall back to 0.
70
+ def usdt_prices
71
+ connection.get("/api/v2/spot/market/tickers").body["data"].to_h { |t| [t["symbol"], t["lastPr"]] }
72
+ rescue StandardError
73
+ {}
74
+ end
75
+
47
76
  def connection
48
77
  @connection ||= build_connection(BASE_URL)
49
78
  end
@@ -5,11 +5,6 @@ module Honeymaker
5
5
  class Hyperliquid < Exchange
6
6
  BASE_URL = "https://api.hyperliquid.xyz"
7
7
 
8
- # Hyperliquid enforces a hard 10-USDC minimum order value on every spot pair (spotMeta exposes
9
- # no per-token base floor). Surfaced as minimum_quote_size so consumers skip/reject sub-10
10
- # orders up front instead of having the exchange reject them.
11
- MINIMUM_QUOTE_SIZE = 10
12
-
13
8
  def get_tickers_info
14
9
  with_rescue do
15
10
  response = connection.post("/info") do |req|
@@ -30,7 +25,7 @@ module Honeymaker
30
25
  base: base_token["name"],
31
26
  quote: quote_token["name"],
32
27
  minimum_base_size: nil,
33
- minimum_quote_size: MINIMUM_QUOTE_SIZE,
28
+ minimum_quote_size: Floors.minimum_quote(venue: "hyperliquid", quote: quote_token["name"]),
34
29
  maximum_base_size: nil,
35
30
  maximum_quote_size: nil,
36
31
  base_decimals: base_token["szDecimals"] || 0,
@@ -27,25 +27,6 @@ module Honeymaker
27
27
  }
28
28
  ].freeze
29
29
 
30
- REAL_COSTMIN = {
31
- "AUD" => 10,
32
- "CAD" => 5,
33
- "CHF" => 5,
34
- "DAI" => 5,
35
- "ETH" => 0.002,
36
- "EUR" => 0.5,
37
- "GBP" => 5,
38
- "JPY" => 500,
39
- "PYUSD" => 5,
40
- "RLUSD" => 5,
41
- "USD" => 5,
42
- "USDC" => 5,
43
- "USDQ" => 5,
44
- "USDR" => 5,
45
- "USDT" => 5,
46
- "XBT" => 0.00005
47
- }.freeze
48
-
49
30
  def get_tickers_info
50
31
  with_rescue do
51
32
  # aclass_base=all, not a bare call: the default response carries only the "currency" class,
@@ -74,7 +55,7 @@ module Honeymaker
74
55
  base: base,
75
56
  quote: quote,
76
57
  minimum_base_size: info["ordermin"],
77
- minimum_quote_size: (REAL_COSTMIN[quote] || info["costmin"] || 0).to_s,
58
+ minimum_quote_size: Floors.minimum_quote(venue: "kraken", quote: quote, published: info["costmin"] || 0).to_s,
78
59
  maximum_base_size: nil,
79
60
  maximum_quote_size: nil,
80
61
  base_decimals: info["lot_decimals"],
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ module Honeymaker
6
+ # Minimum order values venues enforce but do not publish (or publish too low), found by test
7
+ # orders. A ticker's minimum_quote_size is max(published, floor): a floor never lowers a minimum
8
+ # the venue itself raised. Consumed by the venue parsers here and by data-api for venues it
9
+ # builds itself (Alpaca crypto).
10
+ module Floors
11
+ ANY_QUOTE = :any
12
+
13
+ # [venue, asset_class] => { quote => floor }. A nil asset class covers every class the venue
14
+ # lists; ANY_QUOTE covers every quote.
15
+ MINIMUM_QUOTE = {
16
+ # Kraken's AssetPairs costmin is below what AddOrder accepts ("EOrder:Cost minimum not met");
17
+ # these are the per-quote costs it actually enforces. The native core mirrors this table
18
+ # (crates/honeymaker/src/kraken/normalize.rs REAL_COSTMIN; parity-tested).
19
+ ["kraken", nil] => {
20
+ "AUD" => 10,
21
+ "CAD" => 5,
22
+ "CHF" => 5,
23
+ "DAI" => 5,
24
+ "ETH" => 0.002,
25
+ "EUR" => 0.5,
26
+ "GBP" => 5,
27
+ "JPY" => 500,
28
+ "PYUSD" => 5,
29
+ "RLUSD" => 5,
30
+ "USD" => 5,
31
+ "USDC" => 5,
32
+ "USDQ" => 5,
33
+ "USDR" => 5,
34
+ "USDT" => 5,
35
+ "XBT" => 0.00005
36
+ },
37
+ # "Order must have minimum value of 10 USDC." on every spot pair; spotMeta publishes no
38
+ # minimum. The value is measured in USDC whatever the pair's quote, so it covers all quotes.
39
+ ["hyperliquid", nil] => { ANY_QUOTE => 10 },
40
+ # "cost basis must be >= minimal amount of order 10" on crypto; the assets API publishes only
41
+ # min_order_size (base). Stocks are not floored here (notional orders from $1).
42
+ ["alpaca", "crypto"] => { "USD" => 10 }
43
+ }.freeze
44
+
45
+ # The minimum quote size to store: max(published, floor). Returns published when no floor
46
+ # applies (nil when neither exists), and the floor when published is missing or not a number.
47
+ # Whichever wins is returned as given, so callers keep their own formatting.
48
+ def self.minimum_quote(venue:, quote:, asset_class: nil, published: nil)
49
+ quotes = MINIMUM_QUOTE[[venue.to_s, asset_class&.to_s]] || MINIMUM_QUOTE[[venue.to_s, nil]] || {}
50
+ floor = quotes.fetch(quote.to_s) { quotes[ANY_QUOTE] }
51
+ return published if floor.nil?
52
+ return floor if published.nil?
53
+
54
+ BigDecimal(published.to_s) > BigDecimal(floor.to_s) ? published : floor
55
+ rescue ArgumentError
56
+ floor
57
+ end
58
+ end
59
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Honeymaker
4
- VERSION = "0.12.5"
4
+ VERSION = "0.12.6"
5
5
  end
data/lib/honeymaker.rb CHANGED
@@ -7,6 +7,7 @@ require "json"
7
7
  require_relative "honeymaker/version"
8
8
  require_relative "honeymaker/result"
9
9
  require_relative "honeymaker/utils"
10
+ require_relative "honeymaker/floors"
10
11
  require_relative "honeymaker/exchange"
11
12
  require_relative "honeymaker/client"
12
13
  require_relative "honeymaker/native"
@@ -23,7 +23,7 @@ class Honeymaker::Exchanges::BingXTest < Minitest::Test
23
23
  assert_equal "BTC-USDT", ticker[:ticker]
24
24
  assert_equal "BTC", ticker[:base]
25
25
  assert_equal "USDT", ticker[:quote]
26
- assert_equal "0.00001", ticker[:minimum_base_size]
26
+ assert_equal "0", ticker[:minimum_base_size] # deprecated minQty (0.00001) ignored
27
27
  assert_equal "1", ticker[:minimum_quote_size]
28
28
  assert_equal "500", ticker[:maximum_base_size]
29
29
  assert_equal 5, ticker[:base_decimals]
@@ -54,8 +54,60 @@ class Honeymaker::Exchanges::BitgetTest < Minitest::Test
54
54
  assert_equal BigDecimal("67125.67"), result.data[:ask]
55
55
  end
56
56
 
57
+ def test_usdt_minimum_on_a_usdt_pair_is_not_converted_and_no_prices_are_read
58
+ connection = stub
59
+ connection.expects(:get).with("/api/v2/spot/public/symbols").returns(stub(body: load_fixture("bitget_symbols.json")))
60
+ @exchange.instance_variable_set(:@connection, connection)
61
+
62
+ assert_equal "5", @exchange.get_tickers_info.data.first[:minimum_quote_size]
63
+ end
64
+
65
+ def test_non_usdt_quote_converted_at_the_quotes_own_usdt_price
66
+ tickers = [{ "symbol" => "BTCUSDT", "lastPr" => "50000" },
67
+ { "symbol" => "ETHBTC", "lastPr" => "0.05" }] # the traded pair's price: never used
68
+ data = quote_minimums({ "ETHBTC" => "BTC" }, tickers)
69
+ assert_equal BigDecimal("0.0001"), BigDecimal(data["ETHBTC"])
70
+ end
71
+
72
+ def test_inverse_usdt_quote_pair_multiplies
73
+ data = quote_minimums({ "BTCTRY" => "TRY" }, [{ "symbol" => "USDTTRY", "lastPr" => "40" }])
74
+ assert_equal BigDecimal("200"), BigDecimal(data["BTCTRY"])
75
+ end
76
+
77
+ def test_missing_price_leaves_the_minimum_to_the_venue
78
+ data = quote_minimums({ "ETHBTC" => "BTC", "XEUR" => "EUR" },
79
+ [{ "symbol" => "BTCUSDT", "lastPr" => "0" }, { "symbol" => "ETHBTC", "lastPr" => "0.05" }])
80
+ assert_equal "0", data["ETHBTC"]
81
+ assert_equal "0", data["XEUR"]
82
+ end
83
+
84
+ def test_failed_price_read_falls_back_like_a_missing_price
85
+ data = quote_minimums({ "ETHBTC" => "BTC", "BTCUSDT" => "USDT" }, Faraday::ServerError.new("boom"))
86
+ assert_equal "0", data["ETHBTC"]
87
+ assert_equal "5", data["BTCUSDT"]
88
+ end
89
+
57
90
  private
58
91
 
92
+ # symbol => quote rows, each with minTradeUSDT 5; tickers is the market's ticker list or the
93
+ # error its read raises. Returns symbol => minimum_quote_size.
94
+ def quote_minimums(pairs, tickers)
95
+ products = pairs.map do |symbol, quote|
96
+ load_fixture("bitget_symbols.json")["data"].first.merge("symbol" => symbol, "quoteCoin" => quote, "minTradeUSDT" => "5")
97
+ end
98
+ connection = stub
99
+ connection.stubs(:get).with("/api/v2/spot/public/symbols").returns(stub(body: { "data" => products }))
100
+ if tickers.is_a?(Exception)
101
+ connection.stubs(:get).with("/api/v2/spot/market/tickers").raises(tickers)
102
+ else
103
+ connection.stubs(:get).with("/api/v2/spot/market/tickers").returns(stub(body: { "data" => tickers }))
104
+ end
105
+ @exchange.instance_variable_set(:@connection, connection)
106
+ result = @exchange.get_tickers_info
107
+ assert result.success?, result.errors.inspect
108
+ result.data.to_h { |t| [t[:ticker], t[:minimum_quote_size]] }
109
+ end
110
+
59
111
  def stub_connection(body)
60
112
  response = stub(body: body)
61
113
  connection = stub
@@ -123,6 +123,18 @@ class Honeymaker::Exchanges::KrakenTest < Minitest::Test
123
123
  assert_equal "5", ticker[:minimum_quote_size]
124
124
  end
125
125
 
126
+ def test_cost_floor_is_a_max_of_published_and_floor
127
+ body = load_fixture("kraken_asset_pairs.json")
128
+ key, info = body["result"].find { |_, i| i["wsname"]&.end_with?("/USDT") }
129
+ body["result"] = { key => info.merge("costmin" => "0.5") }
130
+ stub_request(body)
131
+ assert_equal "5", @exchange.get_tickers_info.data.first[:minimum_quote_size]
132
+
133
+ body["result"] = { key => info.merge("costmin" => "12.5") }
134
+ stub_request(body)
135
+ assert_equal "12.5", @exchange.get_tickers_info.data.first[:minimum_quote_size]
136
+ end
137
+
126
138
  def test_get_tickers_info_handles_api_error
127
139
  body = { "error" => ["EGeneral:Internal error"], "result" => {} }
128
140
  stub_request(body)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::FloorsTest < Minitest::Test
6
+ F = Honeymaker::Floors
7
+
8
+ def test_max_of_published_and_floor
9
+ assert_equal 5, F.minimum_quote(venue: "kraken", quote: "USD", published: "0.5")
10
+ assert_equal "12.5", F.minimum_quote(venue: "kraken", quote: "USD", published: "12.5")
11
+ assert_equal 5, F.minimum_quote(venue: "kraken", quote: "USD", published: "5.0") # tie: the floor
12
+ end
13
+
14
+ def test_missing_or_unreadable_published_gives_the_floor
15
+ assert_equal 5, F.minimum_quote(venue: "kraken", quote: "USD")
16
+ assert_equal 5, F.minimum_quote(venue: "kraken", quote: "USD", published: "abc")
17
+ end
18
+
19
+ def test_no_floor_keeps_published
20
+ assert_equal "0.1", F.minimum_quote(venue: "kraken", quote: "FOO", published: "0.1")
21
+ assert_equal "3", F.minimum_quote(venue: "binance", quote: "USDT", published: "3")
22
+ assert_nil F.minimum_quote(venue: "binance", quote: "USDT")
23
+ end
24
+
25
+ def test_alpaca_floor_is_keyed_by_asset_class
26
+ assert_equal 10, F.minimum_quote(venue: "alpaca", quote: "USD", asset_class: "crypto", published: "1")
27
+ assert_equal 10, F.minimum_quote(venue: :alpaca, quote: "USD", asset_class: :crypto)
28
+ assert_equal "1", F.minimum_quote(venue: "alpaca", quote: "USD", asset_class: "stock", published: "1")
29
+ assert_nil F.minimum_quote(venue: "alpaca", quote: "USD", asset_class: "stock")
30
+ end
31
+
32
+ def test_hyperliquid_floor_covers_every_quote
33
+ assert_equal 10, F.minimum_quote(venue: "hyperliquid", quote: "USDC")
34
+ assert_equal 10, F.minimum_quote(venue: "hyperliquid", quote: "USDT0", published: "1")
35
+ assert_equal "25", F.minimum_quote(venue: "hyperliquid", quote: "USDC", published: "25")
36
+ end
37
+ end
@@ -43,6 +43,32 @@ class KrakenExchangeParityTest < Minitest::Test
43
43
  [500, JSON_CT, "{}"]].each { |reply| assert_parity(reply, &:get_tickers_info) }
44
44
  end
45
45
 
46
+ # The native REAL_COSTMIN mirrors Honeymaker::Floors: every floored quote, published below,
47
+ # above, equal to, missing and unreadable, plus an unfloored quote.
48
+ def test_cost_floors_match_the_shared_table
49
+ floors = Honeymaker::Floors::MINIMUM_QUOTE[["kraken", nil]]
50
+ pairs = {}
51
+ (floors.keys + ["FOO"]).each do |quote|
52
+ floor = floors[quote] || 1
53
+ [(BigDecimal(floor.to_s) / 10).to_s("F"), (BigDecimal(floor.to_s) * 10).to_s("F"), floor.to_s, nil, "abc", floor * 3].each_with_index do |costmin, i|
54
+ ws = "B#{i}/#{quote}"
55
+ pairs[ws] = { "altname" => ws, "wsname" => ws, "costmin" => costmin }
56
+ end
57
+ end
58
+ reply = [200, JSON_CT, { "error" => [], "result" => pairs }.to_json]
59
+ assert_parity(reply, &:get_tickers_info)
60
+
61
+ d = VenueDouble.new([reply])
62
+ e = Honeymaker::Native::KrakenExchange.new
63
+ e.instance_variable_set(:@connection, e.send(:build_connection, d.url))
64
+ got = e.get_tickers_info.data.to_h { |t| [t[:ticker], t[:minimum_quote_size]] }
65
+ assert_equal floors["USD"].to_s, got["B0/USD"]
66
+ assert_equal "50.0", got["B1/USD"]
67
+ assert_equal "0.1", got["B0/FOO"]
68
+ ensure
69
+ d&.close
70
+ end
71
+
46
72
  def test_one_malformed_row_fails_the_whole_catalogue_like_legacy
47
73
  replies = [false, 1, { "x" => 1 }].map do |bad|
48
74
  pairs = { "XXBTZUSD" => { "altname" => "XBTUSD", "wsname" => "XBT/USD", "status" => "online" },
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: honeymaker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.5
4
+ version: 0.12.6
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Deltabadger
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-16 00:00:00.000000000 Z
11
+ date: 2026-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -131,6 +131,7 @@ files:
131
131
  - lib/honeymaker/exchanges/kraken.rb
132
132
  - lib/honeymaker/exchanges/kucoin.rb
133
133
  - lib/honeymaker/exchanges/mexc.rb
134
+ - lib/honeymaker/floors.rb
134
135
  - lib/honeymaker/native.rb
135
136
  - lib/honeymaker/native/kraken.rb
136
137
  - lib/honeymaker/result.rb
@@ -204,6 +205,7 @@ files:
204
205
  - test/honeymaker/exchanges/kraken_test.rb
205
206
  - test/honeymaker/exchanges/kucoin_test.rb
206
207
  - test/honeymaker/exchanges/mexc_test.rb
208
+ - test/honeymaker/floors_test.rb
207
209
  - test/honeymaker/honeymaker_test.rb
208
210
  - test/honeymaker/result_test.rb
209
211
  - test/honeymaker/utils_test.rb