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 +4 -4
- data/lib/honeymaker/clients/hyperliquid.rb +24 -2
- data/lib/honeymaker/clients/kraken.rb +6 -2
- data/lib/honeymaker/exchanges/kraken.rb +22 -4
- data/lib/honeymaker/version.rb +1 -1
- data/test/fixtures/kraken_asset_pairs.json +28 -1
- data/test/honeymaker/clients/hyperliquid_client_test.rb +70 -0
- data/test/honeymaker/clients/kraken_client_test.rb +30 -0
- data/test/honeymaker/exchanges/kraken_test.rb +42 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d279d1416ac845778e5bf48218fbacf23498cae10bc1ed589cae28752aa4af8c
|
|
4
|
+
data.tar.gz: 2aebe8b36d740f1c09bb9ecf21ab205b911f6ce11fc41b0a7bc80898602d016f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/honeymaker/version.rb
CHANGED
|
@@ -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
|
-
|
|
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
|