honeymaker 0.12.4-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.
Files changed (136) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/native-gems.yml +100 -0
  3. data/.github/workflows/release.yml +36 -0
  4. data/.github/workflows/test.yml +33 -0
  5. data/LICENSE +21 -0
  6. data/README.md +219 -0
  7. data/Rakefile +78 -0
  8. data/crates/honeymaker/tests/vectors/decimal.json +146 -0
  9. data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
  10. data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
  11. data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
  12. data/lib/honeymaker/3.4/honeymaker_native.so +0 -0
  13. data/lib/honeymaker/4.0/honeymaker_native.so +0 -0
  14. data/lib/honeymaker/client.rb +139 -0
  15. data/lib/honeymaker/clients/binance.rb +687 -0
  16. data/lib/honeymaker/clients/binance_us.rb +42 -0
  17. data/lib/honeymaker/clients/bingx.rb +242 -0
  18. data/lib/honeymaker/clients/bitget.rb +318 -0
  19. data/lib/honeymaker/clients/bitrue.rb +245 -0
  20. data/lib/honeymaker/clients/bitvavo.rb +244 -0
  21. data/lib/honeymaker/clients/bybit.rb +308 -0
  22. data/lib/honeymaker/clients/coinbase.rb +326 -0
  23. data/lib/honeymaker/clients/gemini.rb +189 -0
  24. data/lib/honeymaker/clients/hyperliquid.rb +232 -0
  25. data/lib/honeymaker/clients/kraken.rb +379 -0
  26. data/lib/honeymaker/clients/kraken_futures.rb +78 -0
  27. data/lib/honeymaker/clients/kucoin.rb +310 -0
  28. data/lib/honeymaker/clients/mexc.rb +249 -0
  29. data/lib/honeymaker/exchange.rb +129 -0
  30. data/lib/honeymaker/exchanges/binance.rb +55 -0
  31. data/lib/honeymaker/exchanges/binance_us.rb +9 -0
  32. data/lib/honeymaker/exchanges/bingx.rb +56 -0
  33. data/lib/honeymaker/exchanges/bitget.rb +52 -0
  34. data/lib/honeymaker/exchanges/bitrue.rb +53 -0
  35. data/lib/honeymaker/exchanges/bitvavo.rb +65 -0
  36. data/lib/honeymaker/exchanges/bybit.rb +57 -0
  37. data/lib/honeymaker/exchanges/coinbase.rb +69 -0
  38. data/lib/honeymaker/exchanges/gemini.rb +134 -0
  39. data/lib/honeymaker/exchanges/hyperliquid.rb +67 -0
  40. data/lib/honeymaker/exchanges/kraken.rb +122 -0
  41. data/lib/honeymaker/exchanges/kucoin.rb +52 -0
  42. data/lib/honeymaker/exchanges/mexc.rb +56 -0
  43. data/lib/honeymaker/native/kraken.rb +263 -0
  44. data/lib/honeymaker/native.rb +42 -0
  45. data/lib/honeymaker/result.rb +46 -0
  46. data/lib/honeymaker/utils.rb +39 -0
  47. data/lib/honeymaker/version.rb +5 -0
  48. data/lib/honeymaker.rb +97 -0
  49. data/script/parity/kraken_client_live.rb +191 -0
  50. data/script/parity/kraken_live.rb +70 -0
  51. data/script/parity/kraken_transport.rb +604 -0
  52. data/script/parity/resolve.c +49 -0
  53. data/script/release/push_gems.sh +28 -0
  54. data/script/vectors/kraken.rb +50 -0
  55. data/script/vectors/kraken_stage2.rb +146 -0
  56. data/sig/honeymaker.rbs +4 -0
  57. data/test/fixtures/binance_book_ticker.json +7 -0
  58. data/test/fixtures/binance_exchange_info.json +30 -0
  59. data/test/fixtures/bingx_symbols.json +26 -0
  60. data/test/fixtures/bingx_ticker.json +12 -0
  61. data/test/fixtures/bitget_symbols.json +28 -0
  62. data/test/fixtures/bitget_tickers.json +14 -0
  63. data/test/fixtures/bitmart_symbols.json +26 -0
  64. data/test/fixtures/bitmart_ticker.json +12 -0
  65. data/test/fixtures/bitrue_book_ticker.json +7 -0
  66. data/test/fixtures/bitrue_exchange_info.json +34 -0
  67. data/test/fixtures/bitvavo_book_ticker.json +7 -0
  68. data/test/fixtures/bitvavo_markets.json +30 -0
  69. data/test/fixtures/bybit_instruments.json +23 -0
  70. data/test/fixtures/bybit_tickers.json +19 -0
  71. data/test/fixtures/coinbase_product.json +7 -0
  72. data/test/fixtures/coinbase_products.json +24 -0
  73. data/test/fixtures/gemini_pubticker.json +10 -0
  74. data/test/fixtures/gemini_symbol_detail.json +10 -0
  75. data/test/fixtures/gemini_symbols.json +1 -0
  76. data/test/fixtures/hyperliquid_all_mids.json +5 -0
  77. data/test/fixtures/hyperliquid_spot_meta.json +12 -0
  78. data/test/fixtures/kraken_asset_pairs.json +43 -0
  79. data/test/fixtures/kraken_ticker.json +16 -0
  80. data/test/fixtures/kucoin_orderbook_level1.json +10 -0
  81. data/test/fixtures/kucoin_symbols.json +17 -0
  82. data/test/fixtures/mexc_book_ticker.json +7 -0
  83. data/test/fixtures/mexc_exchange_info.json +64 -0
  84. data/test/honeymaker/client_test.rb +90 -0
  85. data/test/honeymaker/clients/binance_client_test.rb +151 -0
  86. data/test/honeymaker/clients/binance_us_client_test.rb +25 -0
  87. data/test/honeymaker/clients/bingx_client_test.rb +106 -0
  88. data/test/honeymaker/clients/bitget_client_test.rb +146 -0
  89. data/test/honeymaker/clients/bitrue_client_test.rb +69 -0
  90. data/test/honeymaker/clients/bitvavo_client_test.rb +156 -0
  91. data/test/honeymaker/clients/bybit_client_test.rb +137 -0
  92. data/test/honeymaker/clients/coinbase_client_test.rb +153 -0
  93. data/test/honeymaker/clients/gemini_client_test.rb +100 -0
  94. data/test/honeymaker/clients/honeymaker_client_registry_test.rb +39 -0
  95. data/test/honeymaker/clients/hyperliquid_client_test.rb +292 -0
  96. data/test/honeymaker/clients/kraken_client_test.rb +255 -0
  97. data/test/honeymaker/clients/kraken_futures_client_test.rb +54 -0
  98. data/test/honeymaker/clients/kucoin_client_test.rb +149 -0
  99. data/test/honeymaker/clients/mexc_client_test.rb +98 -0
  100. data/test/honeymaker/clients/validation_test.rb +167 -0
  101. data/test/honeymaker/error_chain_test.rb +133 -0
  102. data/test/honeymaker/exchange_test.rb +138 -0
  103. data/test/honeymaker/exchanges/binance_test.rb +83 -0
  104. data/test/honeymaker/exchanges/binance_us_test.rb +53 -0
  105. data/test/honeymaker/exchanges/bingx_test.rb +65 -0
  106. data/test/honeymaker/exchanges/bitget_test.rb +65 -0
  107. data/test/honeymaker/exchanges/bitrue_test.rb +65 -0
  108. data/test/honeymaker/exchanges/bitvavo_test.rb +65 -0
  109. data/test/honeymaker/exchanges/bybit_test.rb +55 -0
  110. data/test/honeymaker/exchanges/coinbase_test.rb +106 -0
  111. data/test/honeymaker/exchanges/gemini_catalogue_test.rb +118 -0
  112. data/test/honeymaker/exchanges/gemini_test.rb +184 -0
  113. data/test/honeymaker/exchanges/hyperliquid_test.rb +64 -0
  114. data/test/honeymaker/exchanges/kraken_test.rb +191 -0
  115. data/test/honeymaker/exchanges/kucoin_test.rb +55 -0
  116. data/test/honeymaker/exchanges/mexc_test.rb +97 -0
  117. data/test/honeymaker/honeymaker_test.rb +27 -0
  118. data/test/honeymaker/result_test.rb +42 -0
  119. data/test/honeymaker/utils_test.rb +64 -0
  120. data/test/native/backend_switch_test.rb +53 -0
  121. data/test/native/convert_test.rb +137 -0
  122. data/test/native/gem_isolation_test.rb +17 -0
  123. data/test/native/loader_test.rb +29 -0
  124. data/test/native/lockstep_test.rb +23 -0
  125. data/test/native/signing_smoke_test.rb +28 -0
  126. data/test/parity/kraken_client_parity_test.rb +398 -0
  127. data/test/parity/kraken_exchange_parity_test.rb +172 -0
  128. data/test/release/branch_hardening_test.rb +34 -0
  129. data/test/release/push_gems_test.rb +79 -0
  130. data/test/smoke.rb +24 -0
  131. data/test/support/fixture_helper.rb +13 -0
  132. data/test/support/kraken_signing_smoke.rb +21 -0
  133. data/test/support/parity_helper.rb +62 -0
  134. data/test/support/venue_double.rb +100 -0
  135. data/test/test_helper.rb +9 -0
  136. metadata +254 -0
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Clients
5
+ class Gemini < Client
6
+ URL = "https://api.gemini.com"
7
+ RATE_LIMITS = { default: 200, orders: 200 }.freeze
8
+
9
+ def get_symbols
10
+ get_public("/v1/symbols")
11
+ end
12
+
13
+ def get_symbol_details(symbol:)
14
+ get_public("/v1/symbols/details/#{symbol}")
15
+ end
16
+
17
+ def get_ticker(symbol:)
18
+ get_public("/v1/pubticker/#{symbol}")
19
+ end
20
+
21
+ def get_candles(symbol:, time_frame:)
22
+ get_public("/v2/candles/#{symbol}/#{time_frame}")
23
+ end
24
+
25
+ def get_price_feed
26
+ get_public("/v1/pricefeed")
27
+ end
28
+
29
+ def get_approved_addresses(network:)
30
+ post_signed("/v1/approvedAddresses/account/#{network}")
31
+ end
32
+
33
+ def get_raw_balances
34
+ post_signed("/v1/balances")
35
+ end
36
+
37
+ def get_balances
38
+ result = get_raw_balances
39
+ return result if result.failure?
40
+
41
+ balances = {}
42
+ Array(result.data).each do |balance|
43
+ symbol = balance["currency"]&.upcase
44
+ next unless symbol
45
+ available = BigDecimal((balance["available"] || "0").to_s)
46
+ amount = BigDecimal((balance["amount"] || "0").to_s)
47
+ locked = amount - available
48
+ next if available.zero? && locked.zero?
49
+ balances[symbol] = { free: available, locked: locked }
50
+ end
51
+
52
+ Result::Success.new(balances)
53
+ end
54
+
55
+ def new_order(symbol:, amount:, price:, side:, type:, client_order_id: nil, options: [])
56
+ result = post_signed("/v1/order/new", {
57
+ symbol: symbol, amount: amount, price: price,
58
+ side: side, type: type, client_order_id: client_order_id,
59
+ options: options
60
+ })
61
+ return result if result.failure?
62
+
63
+ raw = result.data
64
+ Result::Success.new({ order_id: raw["order_id"].to_s, raw: raw })
65
+ end
66
+
67
+ def order_status(order_id:)
68
+ result = post_signed("/v1/order/status", { order_id: order_id })
69
+ return result if result.failure?
70
+
71
+ raw = result.data
72
+ Result::Success.new(normalize_order(raw["order_id"].to_s, raw))
73
+ end
74
+
75
+ def cancel_order(order_id:)
76
+ post_signed("/v1/order/cancel", { order_id: order_id })
77
+ end
78
+
79
+ def get_my_trades(symbol: nil, limit_trades: nil, timestamp: nil)
80
+ post_signed("/v1/mytrades", { symbol: symbol, limit_trades: limit_trades, timestamp: timestamp })
81
+ end
82
+
83
+ def get_transfers(timestamp: nil, limit_transfers: nil)
84
+ post_signed("/v1/transfers", { timestamp: timestamp, limit_transfers: limit_transfers })
85
+ end
86
+
87
+ def withdraw(currency:, address:, amount:)
88
+ post_signed("/v1/withdraw/#{currency}", { address: address, amount: amount })
89
+ end
90
+
91
+ # --- Staking ---
92
+
93
+ def staking_history
94
+ post_signed("/v1/staking/history")
95
+ end
96
+
97
+ def staking_rewards
98
+ post_signed("/v1/staking/rewards")
99
+ end
100
+
101
+ def staking_balances
102
+ post_signed("/v1/balances/staking")
103
+ end
104
+
105
+ private
106
+
107
+ def normalize_order(order_id, raw)
108
+ order_type = parse_order_type(raw["type"])
109
+ side = raw["side"]&.downcase&.to_sym
110
+ status = parse_gemini_order_status(raw)
111
+
112
+ price = BigDecimal((raw["avg_execution_price"] || "0").to_s)
113
+ price = BigDecimal((raw["price"] || "0").to_s) if price.zero?
114
+ price = nil if price.zero?
115
+
116
+ amount = BigDecimal((raw["original_amount"] || "0").to_s)
117
+ amount_exec = BigDecimal((raw["executed_amount"] || "0").to_s)
118
+ quote_amount_exec = price ? (amount_exec * price) : BigDecimal("0")
119
+
120
+ {
121
+ order_id: order_id, status: status, side: side, order_type: order_type,
122
+ price: price, amount: amount, quote_amount: nil,
123
+ amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
124
+ }
125
+ end
126
+
127
+ def parse_order_type(type)
128
+ case type&.downcase
129
+ when "exchange limit", "limit" then :limit
130
+ when "market", "exchange market" then :market
131
+ else :limit
132
+ end
133
+ end
134
+
135
+ def parse_gemini_order_status(raw)
136
+ if raw["is_cancelled"]
137
+ :cancelled
138
+ elsif raw["is_live"] && BigDecimal((raw["remaining_amount"] || "0").to_s).positive?
139
+ :open
140
+ elsif !raw["is_live"] && BigDecimal((raw["executed_amount"] || "0").to_s).positive?
141
+ :closed
142
+ else
143
+ :unknown
144
+ end
145
+ end
146
+
147
+ def validate_trading_credentials
148
+ result = get_raw_balances
149
+ return Result::Failure.new("Invalid trading credentials") if result.failure?
150
+ result.data.is_a?(Array) ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
151
+ end
152
+
153
+ def validate_read_credentials
154
+ validate_trading_credentials
155
+ end
156
+
157
+ def get_public(path, params = {})
158
+ with_rescue do
159
+ response = connection.get do |req|
160
+ req.url path
161
+ req.params = params.compact
162
+ end
163
+ response.body
164
+ end
165
+ end
166
+
167
+ def post_signed(path, body = {})
168
+ with_rescue do
169
+ payload = body.compact.merge(request: path, nonce: timestamp_ms.to_s)
170
+ encoded_payload = Base64.strict_encode64(payload.to_json)
171
+ # HMAC-SHA384, not SHA256: https://developer.gemini.com/authentication/api-key
172
+ signature = OpenSSL::HMAC.hexdigest("sha384", @api_secret, encoded_payload)
173
+
174
+ response = connection.post do |req|
175
+ req.url path
176
+ req.headers = {
177
+ "X-GEMINI-APIKEY": @api_key,
178
+ "X-GEMINI-PAYLOAD": encoded_payload,
179
+ "X-GEMINI-SIGNATURE": signature,
180
+ Accept: "application/json",
181
+ "Content-Type": "text/plain"
182
+ }
183
+ end
184
+ response.body
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Clients
5
+ class Hyperliquid < Client
6
+ URL = "https://api.hyperliquid.xyz"
7
+ RATE_LIMITS = { default: 200, orders: 200 }.freeze
8
+
9
+ def initialize(api_key: nil, api_secret: nil, proxy: nil, logger: nil)
10
+ super
11
+ end
12
+
13
+ def spot_meta
14
+ post_info({ type: "spotMeta" })
15
+ end
16
+
17
+ def spot_meta_and_asset_ctxs
18
+ post_info({ type: "spotMetaAndAssetCtxs" })
19
+ end
20
+
21
+ def spot_clearinghouse_state(user:)
22
+ post_info({ type: "spotClearinghouseState", user: user })
23
+ end
24
+
25
+ def all_mids
26
+ post_info({ type: "allMids" })
27
+ end
28
+
29
+ def spot_balances(user: nil)
30
+ user ||= @api_key
31
+ spot_clearinghouse_state(user: user)
32
+ end
33
+
34
+ def l2_book(coin:)
35
+ post_info({ type: "l2Book", coin: coin })
36
+ end
37
+
38
+ def candles_snapshot(coin:, interval:, start_time:, end_time:)
39
+ post_info({ type: "candleSnapshot", req: { coin: coin, interval: interval, startTime: start_time, endTime: end_time } })
40
+ end
41
+
42
+ def get_balances(user: nil)
43
+ user ||= @api_key
44
+ result = spot_clearinghouse_state(user: user)
45
+ return result if result.failure?
46
+
47
+ balances = {}
48
+ (result.data["balances"] || []).each do |balance|
49
+ symbol = balance["coin"]
50
+ total = BigDecimal((balance["total"] || "0").to_s)
51
+ hold = BigDecimal((balance["hold"] || "0").to_s)
52
+ free = total - hold
53
+ next if free.zero? && hold.zero?
54
+ balances[symbol] = { free: free, locked: hold }
55
+ end
56
+
57
+ Result::Success.new(balances)
58
+ end
59
+
60
+ # Hyperliquid's orderStatus body is NESTED:
61
+ # { "status" => "order"|"unknownOid",
62
+ # "order" => { "order" => { coin, side, limitPx, sz(remaining), origSz, oid, timestamp, ... },
63
+ # "status" => <real order status>, "statusTimestamp" => ... } }
64
+ # The real status/sizes live under order["order"]/order["status"] — NOT the top level — and the
65
+ # body carries NO fills. So the ordered amount is origSz, executed is origSz - remaining sz, and the
66
+ # exact cost comes from a bounded userFillsByTime (only fetched when something actually executed,
67
+ # since userFillsByTime is API weight 20 vs orderStatus's weight 2).
68
+ def order_status(user:, oid:)
69
+ result = post_info({ type: "orderStatus", user: user, oid: oid })
70
+ return result if result.failure?
71
+
72
+ raw = result.data
73
+ # A distinct not-found signal — aged-out orders are normal; the caller recovers fills / abandons.
74
+ return Result::Failure.new("unknownOid", data: { not_found: true }) if raw["status"] == "unknownOid"
75
+
76
+ wrapper = raw["order"] || {}
77
+ order = wrapper["order"] || {}
78
+ status_str = wrapper["status"]
79
+
80
+ coin = order["coin"]
81
+ side = order["side"] == "B" ? :buy : :sell
82
+ limit_price = BigDecimal((order["limitPx"] || "0").to_s)
83
+ ordered_size = BigDecimal((order["origSz"] || "0").to_s)
84
+ remaining_size = BigDecimal((order["sz"] || "0").to_s)
85
+ amount_exec = [ordered_size - remaining_size, BigDecimal("0")].max
86
+
87
+ quote_amount_exec = BigDecimal("0")
88
+ price = limit_price
89
+ if amount_exec.positive?
90
+ fills_result = order["timestamp"] ? user_fills_by_time(user: user, start_time: order["timestamp"]) : nil
91
+ # A FAILED exact-cost lookup (timeout / rate-limit) is PROPAGATED so the consumer's typed-error
92
+ # retry runs — never record an executed order with an estimated cost just because userFills
93
+ # blipped (that would silently corrupt accounting and skip the retry).
94
+ return fills_result if fills_result&.failure?
95
+
96
+ matched = Array(fills_result&.data).select { |f| f["oid"].to_s == oid.to_s }
97
+ matched_quote = matched.sum(BigDecimal("0")) { |f| BigDecimal(f["px"].to_s) * BigDecimal(f["sz"].to_s) }
98
+ # userFills SUCCEEDED but has no matching fill (aged out of the window) → estimate from the
99
+ # limit price so a filled order never reports quote_amount_exec 0. Only on success, never failure.
100
+ quote_amount_exec = matched_quote.positive? ? matched_quote : (limit_price * amount_exec)
101
+ price = quote_amount_exec / amount_exec
102
+ end
103
+ price = nil if price.nil? || price.zero?
104
+
105
+ Result::Success.new({
106
+ order_id: "#{coin}-#{oid}", coin: coin,
107
+ status: parse_order_status(status_str), side: side, order_type: :limit,
108
+ price: price, amount: ordered_size, quote_amount: nil,
109
+ amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
110
+ })
111
+ end
112
+
113
+ def open_orders(user:)
114
+ post_info({ type: "openOrders", user: user })
115
+ end
116
+
117
+ def user_fills(user:, start_time: nil, end_time: nil)
118
+ body = { type: "userFills", user: user }
119
+ body[:startTime] = start_time if start_time
120
+ body[:endTime] = end_time if end_time
121
+ post_info(body)
122
+ end
123
+
124
+ def user_fills_by_time(user:, start_time:, end_time: nil)
125
+ body = { type: "userFillsByTime", user: user, startTime: start_time }
126
+ body[:endTime] = end_time if end_time
127
+ post_info(body)
128
+ end
129
+
130
+ # --- Trading (requires hyperliquid-rb gem) ---
131
+
132
+ def order(coin:, is_buy:, size:, limit_px:, order_type: { limit: { tif: "Gtc" } })
133
+ with_status_rescue do
134
+ exchange_client.order(coin, is_buy: is_buy, sz: size, limit_px: limit_px, order_type: order_type)
135
+ end
136
+ end
137
+
138
+ def cancel(coin:, oid:)
139
+ with_status_rescue do
140
+ exchange_client.cancel(coin, oid)
141
+ end
142
+ end
143
+
144
+ # --- Futures ---
145
+
146
+ def user_funding(user:, start_time:, end_time: nil)
147
+ body = { type: "userFunding", user: user, startTime: start_time }
148
+ body[:endTime] = end_time if end_time
149
+ post_info(body)
150
+ end
151
+
152
+ def user_non_funding_ledger_updates(user:, start_time:, end_time: nil)
153
+ body = { type: "userNonFundingLedgerUpdates", user: user, startTime: start_time }
154
+ body[:endTime] = end_time if end_time
155
+ post_info(body)
156
+ end
157
+
158
+ private
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
+
182
+ # Suffix-aware so the whole Hyperliquid cancel family (marginCanceled, scheduledCancel,
183
+ # reduceOnlyCanceled, siblingFilledCanceled, …) maps correctly. A triggered order has fired
184
+ # and become a live resting order → :open. An unmapped status is logged, never swallowed.
185
+ def parse_order_status(status)
186
+ case status
187
+ when "filled" then :closed
188
+ when "open", "triggered" then :open
189
+ else
190
+ str = status.to_s
191
+ if str.match?(/cancel/i) || str.match?(/reject/i)
192
+ :cancelled
193
+ else
194
+ @logger&.warn("[honeymaker] Unmapped Hyperliquid order status: #{status.inspect}")
195
+ :unknown
196
+ end
197
+ end
198
+ end
199
+
200
+ def validate_trading_credentials
201
+ return Result::Failure.new("No wallet address provided") unless @api_key
202
+ result = open_orders(user: @api_key)
203
+ result.success? ? Result::Success.new(true) : Result::Failure.new("Invalid credentials")
204
+ end
205
+
206
+ def validate_read_credentials
207
+ validate_trading_credentials
208
+ end
209
+
210
+ def exchange_client
211
+ raise Error, "Trading requires api_secret (agent key)" unless @api_secret && !@api_secret.empty?
212
+ @exchange ||= begin
213
+ require "hyperliquid"
214
+ ::Hyperliquid::Exchange.new(private_key: @api_secret)
215
+ rescue LoadError
216
+ raise Error, "Add 'hyperliquid-rb' to your Gemfile to use Hyperliquid trading"
217
+ end
218
+ end
219
+
220
+ def post_info(body)
221
+ with_rescue do
222
+ response = connection.post do |req|
223
+ req.url "/info"
224
+ req.headers = { Accept: "application/json", "Content-Type": "application/json" }
225
+ req.body = body.to_json
226
+ end
227
+ response.body
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end