honeymaker 0.11.3 → 0.11.5

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: a33b3f43a40a17e8f8655683b4349901db4222a6ec5db4d4a8f82585f0dacdd1
4
- data.tar.gz: dde27e04419cb12327dfee7c3ecf69c61f36ddf28c1e9b7e2872c785a726106b
3
+ metadata.gz: d279d1416ac845778e5bf48218fbacf23498cae10bc1ed589cae28752aa4af8c
4
+ data.tar.gz: 2aebe8b36d740f1c09bb9ecf21ab205b911f6ce11fc41b0a7bc80898602d016f
5
5
  SHA512:
6
- metadata.gz: f6b7a44553dbc9a413c6df817f4ffbff0f263c077f163e10bce5389d7aba60c108d956c68f9f713ac2b7fef69c699540e3759a1fb5470162146d746ace18a624
7
- data.tar.gz: e53261fd199b590b1d23318d5b2689a59a5b6dc2a402e09b6fb3d63010b1bbb79281e4b3cafd8401964315e51f8a7fbe39e45aa250217e0ed9851c35bc7c494d
6
+ metadata.gz: ae37fa13ff7ce2db5366a408a4d006f671fcbda1c8931c8bca9c8df8a3c13c1256101a73b364bbffc58971923090d5dd8b0e44b49fdc35f73a0ecee8979f67b6
7
+ data.tar.gz: 0a00d2949aeba2b30b681a8021b59083de3498d201446e70ba88e1c2ccfa135f6137036620559f1e79cc2f216328639a3bf61d47f1082fa73040aa60389967c0
@@ -130,13 +130,13 @@ module Honeymaker
130
130
  # --- Trading (requires hyperliquid-rb gem) ---
131
131
 
132
132
  def order(coin:, is_buy:, size:, limit_px:, order_type: { limit: { tif: "Gtc" } })
133
- with_rescue do
133
+ with_status_rescue do
134
134
  exchange_client.order(coin, is_buy: is_buy, sz: size, limit_px: limit_px, order_type: order_type)
135
135
  end
136
136
  end
137
137
 
138
138
  def cancel(coin:, oid:)
139
- with_rescue do
139
+ with_status_rescue do
140
140
  exchange_client.cancel(coin, oid)
141
141
  end
142
142
  end
@@ -157,6 +157,28 @@ module Honeymaker
157
157
 
158
158
  private
159
159
 
160
+ # Trading does not go over this client's Faraday connection: it goes through hyperliquid-rb,
161
+ # which raises its own error for every non-2xx instead of answering. Client#with_rescue can
162
+ # only see an unlabelled StandardError there, so it flags it `client_error` — "no clean
163
+ # answer" — and a caller reading an order placement has to treat that as a request whose fate
164
+ # is unknown: nothing recorded as rejected, no retry, a line for a human to reconcile against
165
+ # the venue. But the status is right there on the exception, and it is exactly what tells a
166
+ # refusal (4xx — the gateway answered, nothing reached the matching engine) from a request the
167
+ # venue may still have taken (5xx). Hand it back in the same shape every other HTTP failure
168
+ # has, so one classifier covers both paths.
169
+ #
170
+ # Duck-typed on purpose: hyperliquid-rb is an optional dependency loaded only when trading is
171
+ # used, so its error classes cannot be named here. Anything without a status is classified
172
+ # exactly as it is everywhere else.
173
+ def with_status_rescue
174
+ Result::Success.new(yield)
175
+ rescue StandardError => e
176
+ status = e.respond_to?(:status) ? e.status.to_i : 0
177
+ return with_rescue { raise e } unless status.positive?
178
+
179
+ Result::Failure.new(e.message.to_s.empty? ? e.class.to_s : e.message, data: { status: status })
180
+ end
181
+
160
182
  # Suffix-aware so the whole Hyperliquid cancel family (marginCanceled, scheduledCancel,
161
183
  # reduceOnlyCanceled, siblingFilledCanceled, …) maps correctly. A triggered order has fired
162
184
  # and become a live resting order → :open. An unmapped status is logged, never swallowed.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Honeymaker
4
- VERSION = "0.11.3"
4
+ VERSION = "0.11.5"
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
  }
@@ -190,6 +190,76 @@ class Honeymaker::Clients::HyperliquidTest < Minitest::Test
190
190
  assert result.success?
191
191
  end
192
192
 
193
+ # == Raised trading errors keep their HTTP status ==
194
+ #
195
+ # Order placement goes through hyperliquid-rb, which raises instead of answering. Whether the
196
+ # venue REFUSED the request or may have TAKEN it is the whole question for a caller deciding
197
+ # between "record a rejected order" and "a human has to go and look" — and the status is the only
198
+ # thing that answers it. It must survive the trip.
199
+
200
+ # hyperliquid-rb's ClientError/ServerError shape. Rebuilt here because the gem is an optional
201
+ # dependency this one does not carry.
202
+ class RaisedWithStatus < StandardError
203
+ attr_reader :status
204
+
205
+ def initialize(message, status:)
206
+ @status = status
207
+ super(message)
208
+ end
209
+ end
210
+
211
+ def test_order_reports_a_refusal_with_its_status
212
+ @client.stubs(:exchange_client).raises(RaisedWithStatus.new("HTTP 429: rate limited", status: 429))
213
+
214
+ result = @client.order(coin: "@142", is_buy: true, size: 1, limit_px: 100)
215
+
216
+ assert result.failure?
217
+ assert_equal 429, result.data[:status]
218
+ assert_nil result.data[:client_error], "the venue answered — this is not an unknown outcome"
219
+ assert_equal ["HTTP 429: rate limited"], result.errors
220
+ end
221
+
222
+ # 5xx stays 5xx: a gateway that may have passed the order on is exactly the case a caller must
223
+ # keep treating as unresolved.
224
+ def test_order_reports_a_gateway_failure_with_its_status
225
+ @client.stubs(:exchange_client).raises(RaisedWithStatus.new("HTTP 502: bad gateway", status: 502))
226
+
227
+ result = @client.order(coin: "@142", is_buy: true, size: 1, limit_px: 100)
228
+
229
+ assert result.failure?
230
+ assert_equal 502, result.data[:status]
231
+ end
232
+
233
+ def test_cancel_reports_a_refusal_with_its_status
234
+ @client.stubs(:exchange_client).raises(RaisedWithStatus.new("HTTP 422: unknown oid", status: 422))
235
+
236
+ result = @client.cancel(coin: "@142", oid: 1)
237
+
238
+ assert result.failure?
239
+ assert_equal 422, result.data[:status]
240
+ end
241
+
242
+ # Everything the venue did not answer is untouched: a signing crash inside the gem, a network
243
+ # failure, a bug here. Those stay flagged as ours and unclassifiable.
244
+ def test_order_leaves_a_statusless_error_classified_as_before
245
+ @client.stubs(:exchange_client).raises(TypeError, "String can't be coerced into Float")
246
+
247
+ result = @client.order(coin: "@142", is_buy: true, size: 1, limit_px: 100)
248
+
249
+ assert result.failure?
250
+ assert result.data[:client_error], "no status means no answer from the venue"
251
+ assert_equal ["TypeError: String can't be coerced into Float"], result.errors
252
+ end
253
+
254
+ def test_order_wraps_an_accepted_placement
255
+ @client.stubs(:exchange_client).returns(stub(order: { "status" => "ok" }))
256
+
257
+ result = @client.order(coin: "@142", is_buy: true, size: 1, limit_px: 100)
258
+
259
+ assert result.success?
260
+ assert_equal({ "status" => "ok" }, result.data)
261
+ end
262
+
193
263
  private
194
264
 
195
265
  def stub_connection(method, body)
@@ -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
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.3
4
+ version: 0.11.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Deltabadger