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,687 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Clients
5
+ class Binance < Client
6
+ URL = "https://api.binance.com"
7
+ RATE_LIMITS = { default: 100, orders: 200 }.freeze
8
+
9
+ def exchange_information(symbol: nil, symbols: nil, permissions: nil, show_permission_sets: nil, symbol_status: nil)
10
+ with_rescue do
11
+ response = connection.get do |req|
12
+ req.url "/api/v3/exchangeInfo"
13
+ req.headers = headers
14
+ req.params = {
15
+ symbol: symbol,
16
+ symbols: symbols&.to_json,
17
+ permissions: permissions&.to_json,
18
+ showPermissionSets: show_permission_sets,
19
+ symbolStatus: symbol_status
20
+ }.compact
21
+ end
22
+ response.body
23
+ end
24
+ end
25
+
26
+ def symbol_price_ticker(symbol: nil, symbols: nil)
27
+ with_rescue do
28
+ response = connection.get do |req|
29
+ req.url "/api/v3/ticker/price"
30
+ req.headers = headers
31
+ req.params = { symbol: symbol, symbols: symbols&.to_json }.compact
32
+ end
33
+ response.body
34
+ end
35
+ end
36
+
37
+ def symbol_order_book_ticker(symbol: nil, symbols: nil)
38
+ with_rescue do
39
+ response = connection.get do |req|
40
+ req.url "/api/v3/ticker/bookTicker"
41
+ req.headers = headers
42
+ req.params = { symbol: symbol, symbols: symbols&.to_json }.compact
43
+ end
44
+ response.body
45
+ end
46
+ end
47
+
48
+ def candlestick_data(symbol:, interval:, start_time: nil, end_time: nil, time_zone: 0, limit: 500)
49
+ with_rescue do
50
+ response = connection.get do |req|
51
+ req.url "/api/v3/klines"
52
+ req.headers = headers
53
+ req.params = {
54
+ symbol: symbol, interval: interval,
55
+ startTime: start_time, endTime: end_time,
56
+ timeZone: time_zone, limit: limit
57
+ }.compact
58
+ end
59
+ response.body
60
+ end
61
+ end
62
+
63
+ def account_information(omit_zero_balances: false, recv_window: 5000)
64
+ with_rescue do
65
+ response = connection.get do |req|
66
+ req.url "/api/v3/account"
67
+ req.headers = headers
68
+ req.params = {
69
+ omitZeroBalances: omit_zero_balances,
70
+ recvWindow: recv_window,
71
+ timestamp: timestamp_ms
72
+ }.compact
73
+ req.params[:signature] = sign_params(req.params)
74
+ end
75
+ response.body
76
+ end
77
+ end
78
+
79
+ def get_balances
80
+ result = account_information(omit_zero_balances: true)
81
+ return result if result.failure?
82
+
83
+ balances = {}
84
+ Array(result.data["balances"]).each do |balance|
85
+ symbol = balance["asset"]
86
+ free = BigDecimal(balance["free"].to_s)
87
+ locked = BigDecimal(balance["locked"].to_s)
88
+ next if free.zero? && locked.zero?
89
+ balances[symbol] = { free: free, locked: locked }
90
+ end
91
+
92
+ Result::Success.new(balances)
93
+ end
94
+
95
+ def account_trade_list(symbol:, order_id: nil, start_time: nil, end_time: nil, from_id: nil, limit: 500, recv_window: 5000)
96
+ with_rescue do
97
+ response = connection.get do |req|
98
+ req.url "/api/v3/myTrades"
99
+ req.headers = headers
100
+ req.params = {
101
+ symbol: symbol, orderId: order_id,
102
+ startTime: start_time, endTime: end_time,
103
+ fromId: from_id, limit: limit,
104
+ recvWindow: recv_window, timestamp: timestamp_ms
105
+ }.compact
106
+ req.params[:signature] = sign_params(req.params)
107
+ end
108
+ response.body
109
+ end
110
+ end
111
+
112
+ def query_order(symbol:, order_id: nil, orig_client_order_id: nil, recv_window: 5000)
113
+ with_rescue do
114
+ response = connection.get do |req|
115
+ req.url "/api/v3/order"
116
+ req.headers = headers
117
+ req.params = {
118
+ symbol: symbol, orderId: order_id,
119
+ origClientOrderId: orig_client_order_id,
120
+ recvWindow: recv_window, timestamp: timestamp_ms
121
+ }.compact
122
+ req.params[:signature] = sign_params(req.params)
123
+ end
124
+ raw = response.body
125
+ normalize_order("#{symbol}-#{raw['orderId']}", raw)
126
+ end
127
+ end
128
+
129
+ def all_orders(symbol:, order_id: nil, start_time: nil, end_time: nil, limit: 500, recv_window: 5000)
130
+ with_rescue do
131
+ response = connection.get do |req|
132
+ req.url "/api/v3/allOrders"
133
+ req.headers = headers
134
+ req.params = {
135
+ symbol: symbol, orderId: order_id,
136
+ startTime: start_time, endTime: end_time,
137
+ limit: limit, recvWindow: recv_window, timestamp: timestamp_ms
138
+ }.compact
139
+ req.params[:signature] = sign_params(req.params)
140
+ end
141
+ response.body.map do |raw|
142
+ normalize_order("#{symbol}-#{raw['orderId']}", raw)
143
+ end
144
+ end
145
+ end
146
+
147
+ def new_order(symbol:, side:, type:, time_in_force: nil, quantity: nil, quote_order_qty: nil,
148
+ price: nil, new_client_order_id: nil, strategy_id: nil, strategy_type: nil,
149
+ stop_price: nil, trailing_delta: nil, iceberg_qty: nil, new_order_resp_type: nil,
150
+ self_trade_prevention_mode: nil, recv_window: 5000)
151
+ with_rescue do
152
+ response = connection.post do |req|
153
+ req.url "/api/v3/order"
154
+ req.headers = headers
155
+ req.params = {
156
+ symbol: symbol, side: side, type: type,
157
+ timeInForce: time_in_force, quantity: quantity,
158
+ quoteOrderQty: quote_order_qty, price: price,
159
+ newClientOrderId: new_client_order_id,
160
+ strategyId: strategy_id, strategyType: strategy_type,
161
+ stopPrice: stop_price, trailingDelta: trailing_delta,
162
+ icebergQty: iceberg_qty, newOrderRespType: new_order_resp_type,
163
+ selfTradePreventionMode: self_trade_prevention_mode,
164
+ recvWindow: recv_window, timestamp: timestamp_ms
165
+ }.compact
166
+ req.params[:signature] = sign_params(req.params)
167
+ end
168
+ raw = response.body
169
+ { order_id: "#{symbol}-#{raw['orderId']}", raw: raw }
170
+ end
171
+ end
172
+
173
+ def test_new_order(symbol:, side:, type:, time_in_force: nil, quantity: nil, quote_order_qty: nil,
174
+ price: nil, new_client_order_id: nil, strategy_id: nil, strategy_type: nil,
175
+ stop_price: nil, trailing_delta: nil, iceberg_qty: nil, new_order_resp_type: nil,
176
+ self_trade_prevention_mode: nil, recv_window: 5000, compute_commission_rates: false)
177
+ with_rescue do
178
+ response = connection.post do |req|
179
+ req.url "/api/v3/order/test"
180
+ req.headers = headers
181
+ req.params = {
182
+ symbol: symbol, side: side, type: type,
183
+ timeInForce: time_in_force, quantity: quantity,
184
+ quoteOrderQty: quote_order_qty, price: price,
185
+ newClientOrderId: new_client_order_id,
186
+ strategyId: strategy_id, strategyType: strategy_type,
187
+ stopPrice: stop_price, trailingDelta: trailing_delta,
188
+ icebergQty: iceberg_qty, newOrderRespType: new_order_resp_type,
189
+ selfTradePreventionMode: self_trade_prevention_mode,
190
+ computeCommissionRates: compute_commission_rates,
191
+ recvWindow: recv_window, timestamp: timestamp_ms
192
+ }.compact
193
+ req.params[:signature] = sign_params(req.params)
194
+ end
195
+ response.body
196
+ end
197
+ end
198
+
199
+ def cancel_order(symbol:, order_id: nil, orig_client_order_id: nil, new_client_order_id: nil,
200
+ cancel_restrictions: nil, recv_window: 5000)
201
+ with_rescue do
202
+ response = connection.delete do |req|
203
+ req.url "/api/v3/order"
204
+ req.headers = headers
205
+ req.params = {
206
+ symbol: symbol, orderId: order_id,
207
+ origClientOrderId: orig_client_order_id,
208
+ newClientOrderId: new_client_order_id,
209
+ cancelRestrictions: cancel_restrictions,
210
+ recvWindow: recv_window, timestamp: timestamp_ms
211
+ }.compact
212
+ req.params[:signature] = sign_params(req.params)
213
+ end
214
+ response.body
215
+ end
216
+ end
217
+
218
+ def deposit_history(coin: nil, status: nil, start_time: nil, end_time: nil, offset: nil, limit: 1000, recv_window: 5000)
219
+ with_rescue do
220
+ response = connection.get do |req|
221
+ req.url "/sapi/v1/capital/deposit/hisrec"
222
+ req.headers = headers
223
+ req.params = {
224
+ coin: coin, status: status,
225
+ startTime: start_time, endTime: end_time,
226
+ offset: offset, limit: limit,
227
+ recvWindow: recv_window, timestamp: timestamp_ms
228
+ }.compact
229
+ req.params[:signature] = sign_params(req.params)
230
+ end
231
+ response.body
232
+ end
233
+ end
234
+
235
+ def withdraw_history(coin: nil, status: nil, start_time: nil, end_time: nil, offset: nil, limit: 1000, recv_window: 5000)
236
+ with_rescue do
237
+ response = connection.get do |req|
238
+ req.url "/sapi/v1/capital/withdraw/history"
239
+ req.headers = headers
240
+ req.params = {
241
+ coin: coin, status: status,
242
+ startTime: start_time, endTime: end_time,
243
+ offset: offset, limit: limit,
244
+ recvWindow: recv_window, timestamp: timestamp_ms
245
+ }.compact
246
+ req.params[:signature] = sign_params(req.params)
247
+ end
248
+ response.body
249
+ end
250
+ end
251
+
252
+ def convert_trade_flow(start_time:, end_time:, limit: 1000, recv_window: 5000)
253
+ with_rescue do
254
+ response = connection.get do |req|
255
+ req.url "/sapi/v1/convert/tradeFlow"
256
+ req.headers = headers
257
+ req.params = {
258
+ startTime: start_time, endTime: end_time,
259
+ limit: limit, recvWindow: recv_window, timestamp: timestamp_ms
260
+ }.compact
261
+ req.params[:signature] = sign_params(req.params)
262
+ end
263
+ response.body
264
+ end
265
+ end
266
+
267
+ def fiat_payments(transaction_type:, begin_time: nil, end_time: nil, page: nil, rows: nil, recv_window: 5000)
268
+ with_rescue do
269
+ response = connection.get do |req|
270
+ req.url "/sapi/v1/fiat/payments"
271
+ req.headers = headers
272
+ req.params = {
273
+ transactionType: transaction_type,
274
+ beginTime: begin_time, endTime: end_time,
275
+ page: page, rows: rows,
276
+ recvWindow: recv_window, timestamp: timestamp_ms
277
+ }.compact
278
+ req.params[:signature] = sign_params(req.params)
279
+ end
280
+ response.body
281
+ end
282
+ end
283
+
284
+ def fiat_orders(transaction_type:, begin_time: nil, end_time: nil, page: nil, rows: nil, recv_window: 5000)
285
+ with_rescue do
286
+ response = connection.get do |req|
287
+ req.url "/sapi/v1/fiat/orders"
288
+ req.headers = headers
289
+ req.params = {
290
+ transactionType: transaction_type,
291
+ beginTime: begin_time, endTime: end_time,
292
+ page: page, rows: rows,
293
+ recvWindow: recv_window, timestamp: timestamp_ms
294
+ }.compact
295
+ req.params[:signature] = sign_params(req.params)
296
+ end
297
+ response.body
298
+ end
299
+ end
300
+
301
+ def dust_log(start_time: nil, end_time: nil, recv_window: 5000)
302
+ with_rescue do
303
+ response = connection.get do |req|
304
+ req.url "/sapi/v1/asset/dribblet"
305
+ req.headers = headers
306
+ req.params = {
307
+ startTime: start_time, endTime: end_time,
308
+ recvWindow: recv_window, timestamp: timestamp_ms
309
+ }.compact
310
+ req.params[:signature] = sign_params(req.params)
311
+ end
312
+ response.body
313
+ end
314
+ end
315
+
316
+ def asset_dividend(asset: nil, start_time: nil, end_time: nil, limit: 500, recv_window: 5000)
317
+ with_rescue do
318
+ response = connection.get do |req|
319
+ req.url "/sapi/v1/asset/assetDividend"
320
+ req.headers = headers
321
+ req.params = {
322
+ asset: asset, startTime: start_time, endTime: end_time,
323
+ limit: limit, recvWindow: recv_window, timestamp: timestamp_ms
324
+ }.compact
325
+ req.params[:signature] = sign_params(req.params)
326
+ end
327
+ response.body
328
+ end
329
+ end
330
+
331
+ def simple_earn_flexible_rewards(asset: nil, start_time: nil, end_time: nil, current: nil, size: nil)
332
+ with_rescue do
333
+ response = connection.get do |req|
334
+ req.url "/sapi/v1/simple-earn/flexible/history/rewardsRecord"
335
+ req.headers = headers
336
+ req.params = {
337
+ asset: asset, startTime: start_time, endTime: end_time,
338
+ current: current, size: size, timestamp: timestamp_ms
339
+ }.compact
340
+ req.params[:signature] = sign_params(req.params)
341
+ end
342
+ response.body
343
+ end
344
+ end
345
+
346
+ def simple_earn_locked_rewards(asset: nil, start_time: nil, end_time: nil, current: nil, size: nil)
347
+ with_rescue do
348
+ response = connection.get do |req|
349
+ req.url "/sapi/v1/simple-earn/locked/history/rewardsRecord"
350
+ req.headers = headers
351
+ req.params = {
352
+ asset: asset, startTime: start_time, endTime: end_time,
353
+ current: current, size: size, timestamp: timestamp_ms
354
+ }.compact
355
+ req.params[:signature] = sign_params(req.params)
356
+ end
357
+ response.body
358
+ end
359
+ end
360
+
361
+ def get_all_coins_information(recv_window: 5000)
362
+ with_rescue do
363
+ response = connection.get do |req|
364
+ req.url "/sapi/v1/capital/config/getall"
365
+ req.headers = headers
366
+ req.params = { recvWindow: recv_window, timestamp: timestamp_ms }
367
+ req.params[:signature] = sign_params(req.params)
368
+ end
369
+ response.body
370
+ end
371
+ end
372
+
373
+ def api_description(recv_window: 5000)
374
+ with_rescue do
375
+ response = connection.get do |req|
376
+ req.url "/sapi/v1/account/apiRestrictions"
377
+ req.headers = headers
378
+ req.params = { recvWindow: recv_window, timestamp: timestamp_ms }
379
+ req.params[:signature] = sign_params(req.params)
380
+ end
381
+ response.body
382
+ end
383
+ end
384
+
385
+ def get_withdraw_addresses(recv_window: 5000)
386
+ with_rescue do
387
+ response = connection.get do |req|
388
+ req.url "/sapi/v1/capital/withdraw/address/list"
389
+ req.headers = headers
390
+ req.params = { recvWindow: recv_window, timestamp: timestamp_ms }
391
+ req.params[:signature] = sign_params(req.params)
392
+ end
393
+ response.body
394
+ end
395
+ end
396
+
397
+ def withdraw(coin:, address:, amount:, network: nil, address_tag: nil, recv_window: 5000)
398
+ with_rescue do
399
+ response = connection.post do |req|
400
+ req.url "/sapi/v1/capital/withdraw/apply"
401
+ req.headers = headers
402
+ req.params = {
403
+ coin: coin, address: address, amount: amount,
404
+ network: network, addressTag: address_tag,
405
+ recvWindow: recv_window, timestamp: timestamp_ms
406
+ }.compact
407
+ req.params[:signature] = sign_params(req.params)
408
+ end
409
+ response.body
410
+ end
411
+ end
412
+
413
+ # --- Margin ---
414
+
415
+ def margin_borrow_repay_history(type:, asset: nil, isolated_symbol: nil, start_time: nil, end_time: nil,
416
+ current: nil, size: nil, recv_window: 5000)
417
+ with_rescue do
418
+ response = connection.get do |req|
419
+ req.url "/sapi/v1/margin/borrow-repay"
420
+ req.headers = headers
421
+ req.params = {
422
+ type: type, asset: asset, isolatedSymbol: isolated_symbol,
423
+ startTime: start_time, endTime: end_time,
424
+ current: current, size: size,
425
+ recvWindow: recv_window, timestamp: timestamp_ms
426
+ }.compact
427
+ req.params[:signature] = sign_params(req.params)
428
+ end
429
+ response.body
430
+ end
431
+ end
432
+
433
+ def margin_interest_history(asset: nil, isolated_symbol: nil, start_time: nil, end_time: nil,
434
+ current: nil, size: nil, archived: nil, recv_window: 5000)
435
+ with_rescue do
436
+ response = connection.get do |req|
437
+ req.url "/sapi/v1/margin/interestHistory"
438
+ req.headers = headers
439
+ req.params = {
440
+ asset: asset, isolatedSymbol: isolated_symbol,
441
+ startTime: start_time, endTime: end_time,
442
+ current: current, size: size, archived: archived,
443
+ recvWindow: recv_window, timestamp: timestamp_ms
444
+ }.compact
445
+ req.params[:signature] = sign_params(req.params)
446
+ end
447
+ response.body
448
+ end
449
+ end
450
+
451
+ def margin_force_liquidation(start_time: nil, end_time: nil, isolated_symbol: nil,
452
+ current: nil, size: nil, recv_window: 5000)
453
+ with_rescue do
454
+ response = connection.get do |req|
455
+ req.url "/sapi/v1/margin/forceLiquidationRec"
456
+ req.headers = headers
457
+ req.params = {
458
+ startTime: start_time, endTime: end_time,
459
+ isolatedSymbol: isolated_symbol,
460
+ current: current, size: size,
461
+ recvWindow: recv_window, timestamp: timestamp_ms
462
+ }.compact
463
+ req.params[:signature] = sign_params(req.params)
464
+ end
465
+ response.body
466
+ end
467
+ end
468
+
469
+ # --- Futures ---
470
+
471
+ def futures_income_history(symbol: nil, income_type: nil, start_time: nil, end_time: nil,
472
+ page: nil, limit: 1000, recv_window: 5000)
473
+ with_rescue do
474
+ response = usdt_futures_connection.get do |req|
475
+ req.url "/fapi/v1/income"
476
+ req.headers = headers
477
+ req.params = {
478
+ symbol: symbol, incomeType: income_type,
479
+ startTime: start_time, endTime: end_time,
480
+ page: page, limit: limit,
481
+ recvWindow: recv_window, timestamp: timestamp_ms
482
+ }.compact
483
+ req.params[:signature] = sign_params(req.params)
484
+ end
485
+ response.body
486
+ end
487
+ end
488
+
489
+ def coin_futures_income_history(symbol: nil, income_type: nil, start_time: nil, end_time: nil,
490
+ page: nil, limit: 1000, recv_window: 5000)
491
+ with_rescue do
492
+ response = coin_futures_connection.get do |req|
493
+ req.url "/dapi/v1/income"
494
+ req.headers = headers
495
+ req.params = {
496
+ symbol: symbol, incomeType: income_type,
497
+ startTime: start_time, endTime: end_time,
498
+ page: page, limit: limit,
499
+ recvWindow: recv_window, timestamp: timestamp_ms
500
+ }.compact
501
+ req.params[:signature] = sign_params(req.params)
502
+ end
503
+ response.body
504
+ end
505
+ end
506
+
507
+ # --- Simple Earn ---
508
+
509
+ def simple_earn_flexible_subscriptions(product_id: nil, purchase_id: nil, asset: nil,
510
+ start_time: nil, end_time: nil, current: nil, size: nil)
511
+ with_rescue do
512
+ response = connection.get do |req|
513
+ req.url "/sapi/v1/simple-earn/flexible/history/subscriptionRecord"
514
+ req.headers = headers
515
+ req.params = {
516
+ productId: product_id, purchaseId: purchase_id, asset: asset,
517
+ startTime: start_time, endTime: end_time,
518
+ current: current, size: size, timestamp: timestamp_ms
519
+ }.compact
520
+ req.params[:signature] = sign_params(req.params)
521
+ end
522
+ response.body
523
+ end
524
+ end
525
+
526
+ def simple_earn_flexible_redemptions(product_id: nil, redeem_id: nil, asset: nil,
527
+ start_time: nil, end_time: nil, current: nil, size: nil)
528
+ with_rescue do
529
+ response = connection.get do |req|
530
+ req.url "/sapi/v1/simple-earn/flexible/history/redemptionRecord"
531
+ req.headers = headers
532
+ req.params = {
533
+ productId: product_id, redeemId: redeem_id, asset: asset,
534
+ startTime: start_time, endTime: end_time,
535
+ current: current, size: size, timestamp: timestamp_ms
536
+ }.compact
537
+ req.params[:signature] = sign_params(req.params)
538
+ end
539
+ response.body
540
+ end
541
+ end
542
+
543
+ def simple_earn_locked_subscriptions(product_id: nil, purchase_id: nil, asset: nil,
544
+ start_time: nil, end_time: nil, current: nil, size: nil)
545
+ with_rescue do
546
+ response = connection.get do |req|
547
+ req.url "/sapi/v1/simple-earn/locked/history/subscriptionRecord"
548
+ req.headers = headers
549
+ req.params = {
550
+ productId: product_id, purchaseId: purchase_id, asset: asset,
551
+ startTime: start_time, endTime: end_time,
552
+ current: current, size: size, timestamp: timestamp_ms
553
+ }.compact
554
+ req.params[:signature] = sign_params(req.params)
555
+ end
556
+ response.body
557
+ end
558
+ end
559
+
560
+ def simple_earn_locked_redemptions(product_id: nil, redeem_id: nil, asset: nil,
561
+ start_time: nil, end_time: nil, current: nil, size: nil)
562
+ with_rescue do
563
+ response = connection.get do |req|
564
+ req.url "/sapi/v1/simple-earn/locked/history/redemptionRecord"
565
+ req.headers = headers
566
+ req.params = {
567
+ productId: product_id, redeemId: redeem_id, asset: asset,
568
+ startTime: start_time, endTime: end_time,
569
+ current: current, size: size, timestamp: timestamp_ms
570
+ }.compact
571
+ req.params[:signature] = sign_params(req.params)
572
+ end
573
+ response.body
574
+ end
575
+ end
576
+
577
+ # --- Transfers ---
578
+
579
+ def universal_transfer_history(type:, start_time: nil, end_time: nil, current: nil, size: nil, recv_window: 5000)
580
+ with_rescue do
581
+ response = connection.get do |req|
582
+ req.url "/sapi/v1/asset/transfer"
583
+ req.headers = headers
584
+ req.params = {
585
+ type: type, startTime: start_time, endTime: end_time,
586
+ current: current, size: size,
587
+ recvWindow: recv_window, timestamp: timestamp_ms
588
+ }.compact
589
+ req.params[:signature] = sign_params(req.params)
590
+ end
591
+ response.body
592
+ end
593
+ end
594
+
595
+ private
596
+
597
+ def usdt_futures_connection
598
+ @usdt_futures_connection ||= build_client_connection("https://fapi.binance.com")
599
+ end
600
+
601
+ def coin_futures_connection
602
+ @coin_futures_connection ||= build_client_connection("https://dapi.binance.com")
603
+ end
604
+
605
+ def normalize_order(order_id, raw)
606
+ order_type = parse_order_type(raw["type"])
607
+ side = raw["side"]&.downcase&.to_sym
608
+ status = parse_order_status(raw["status"])
609
+
610
+ amount = BigDecimal(raw["origQty"].to_s)
611
+ amount = nil if amount.zero?
612
+ quote_amount = BigDecimal(raw["origQuoteOrderQty"].to_s)
613
+ quote_amount = nil if quote_amount.zero?
614
+
615
+ amount_exec = BigDecimal(raw["executedQty"].to_s)
616
+ quote_amount_exec = BigDecimal(raw["cummulativeQuoteQty"].to_s)
617
+ quote_amount_exec = nil if quote_amount_exec.negative?
618
+
619
+ price = BigDecimal(raw["price"].to_s)
620
+ if price.zero? && quote_amount_exec&.positive? && amount_exec.positive?
621
+ price = quote_amount_exec / amount_exec
622
+ end
623
+ price = nil if price.zero?
624
+
625
+ {
626
+ order_id: order_id,
627
+ status: status,
628
+ side: side,
629
+ order_type: order_type,
630
+ price: price,
631
+ amount: amount,
632
+ quote_amount: quote_amount,
633
+ amount_exec: amount_exec,
634
+ quote_amount_exec: quote_amount_exec,
635
+ raw: raw
636
+ }
637
+ end
638
+
639
+ def parse_order_type(type)
640
+ case type
641
+ when "MARKET" then :market
642
+ when "LIMIT" then :limit
643
+ else :unknown
644
+ end
645
+ end
646
+
647
+ def parse_order_status(status)
648
+ case status
649
+ when "PENDING_CANCEL" then :unknown
650
+ when "NEW", "PENDING_NEW", "PARTIALLY_FILLED" then :open
651
+ when "FILLED" then :closed
652
+ when "CANCELED", "EXPIRED", "EXPIRED_IN_MATCH" then :cancelled
653
+ when "REJECTED" then :failed
654
+ else :unknown
655
+ end
656
+ end
657
+
658
+ def validate_trading_credentials
659
+ # Try cancelling a non-existent order — error -2011 (ORDER_DOES_NOT_EXIST) means key is valid with trade permission
660
+ result = cancel_order(symbol: "ETHBTC", order_id: "9999999999")
661
+ return Result::Failure.new("Invalid trading credentials") if result.failure?
662
+
663
+ code = result.data.is_a?(Hash) ? result.data["code"] : nil
664
+ code == -2011 ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
665
+ end
666
+
667
+ def validate_read_credentials
668
+ result = api_description
669
+ Result.new(data: result.success?, errors: result.success? ? [] : ["Invalid read credentials"])
670
+ end
671
+
672
+ def headers
673
+ if authenticated?
674
+ { "X-MBX-APIKEY": @api_key, Accept: "application/json", "Content-Type": "application/json" }
675
+ else
676
+ { Accept: "application/json", "Content-Type": "application/json" }
677
+ end
678
+ end
679
+
680
+ def sign_params(params)
681
+ return unless @api_secret
682
+ query = Faraday::Utils.build_query(params)
683
+ hmac_sha256(@api_secret, query)
684
+ end
685
+ end
686
+ end
687
+ end