honeymaker 0.12.4-x86_64-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,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Clients
5
+ class BinanceUs < Binance
6
+ URL = "https://api.binance.us"
7
+
8
+ # --- Staking ---
9
+
10
+ def staking_history(staking_type: nil, asset: nil, start_time: nil, end_time: nil, page: nil, limit: nil)
11
+ with_rescue do
12
+ response = connection.get do |req|
13
+ req.url "/staking/v1/history"
14
+ req.headers = headers
15
+ req.params = {
16
+ stakingType: staking_type, asset: asset,
17
+ startTime: start_time, endTime: end_time,
18
+ page: page, limit: limit, timestamp: timestamp_ms
19
+ }.compact
20
+ req.params[:signature] = sign_params(req.params)
21
+ end
22
+ response.body
23
+ end
24
+ end
25
+
26
+ def staking_rewards_history(asset: nil, start_time: nil, end_time: nil, page: nil, limit: nil)
27
+ with_rescue do
28
+ response = connection.get do |req|
29
+ req.url "/staking/v1/rewardsHistory"
30
+ req.headers = headers
31
+ req.params = {
32
+ asset: asset, startTime: start_time, endTime: end_time,
33
+ page: page, limit: limit, timestamp: timestamp_ms
34
+ }.compact
35
+ req.params[:signature] = sign_params(req.params)
36
+ end
37
+ response.body
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Clients
5
+ class BingX < Client
6
+ URL = "https://open-api.bingx.com"
7
+ RATE_LIMITS = { default: 100, orders: 200 }.freeze
8
+
9
+ def get_symbols
10
+ get_public("/openApi/spot/v1/common/symbols")
11
+ end
12
+
13
+ def get_ticker(symbol: nil)
14
+ get_public("/openApi/spot/v2/market/ticker", { symbol: symbol })
15
+ end
16
+
17
+ def get_depth(symbol:, limit: nil)
18
+ get_public("/openApi/spot/v1/market/depth", { symbol: symbol, limit: limit })
19
+ end
20
+
21
+ def get_klines(symbol:, interval:, start_time: nil, end_time: nil, limit: nil)
22
+ get_public("/openApi/spot/v1/market/kline", {
23
+ symbol: symbol, interval: interval,
24
+ startTime: start_time, endTime: end_time, limit: limit
25
+ })
26
+ end
27
+
28
+ def get_raw_balances
29
+ get_signed("/openApi/spot/v1/account/balance")
30
+ end
31
+
32
+ def get_balances
33
+ result = get_raw_balances
34
+ return result if result.failure?
35
+
36
+ balances = {}
37
+ raw_balances = result.data.dig("data", "balances") || []
38
+ raw_balances.each do |balance|
39
+ symbol = balance["asset"]
40
+ free = BigDecimal((balance["free"] || "0").to_s)
41
+ locked = BigDecimal((balance["locked"] || "0").to_s)
42
+ next if free.zero? && locked.zero?
43
+ balances[symbol] = { free: free, locked: locked }
44
+ end
45
+
46
+ Result::Success.new(balances)
47
+ end
48
+
49
+ def place_order(symbol:, side:, type:, quantity: nil, quote_order_qty: nil, price: nil,
50
+ time_in_force: nil, client_order_id: nil)
51
+ result = post_signed("/openApi/spot/v1/trade/order", {
52
+ symbol: symbol, side: side, type: type,
53
+ quantity: quantity, quoteOrderQty: quote_order_qty,
54
+ price: price, timeInForce: time_in_force,
55
+ newClientOrderId: client_order_id
56
+ })
57
+ return result if result.failure?
58
+
59
+ raw = result.data
60
+ # BingX rejects with HTTP 200 and a non-zero `code`. Without this the rejection fell
61
+ # through as a Success carrying order_id "#{symbol}-" (nil id): the bot recorded a trade
62
+ # that was never placed, then polled a malformed id forever.
63
+ return api_error("BingX", raw) unless raw.is_a?(Hash) && raw["code"].to_i.zero?
64
+
65
+ order_id = raw.dig("data", "orderId") || raw.dig("data", "data", "orderId")
66
+ Result::Success.new({ order_id: "#{symbol}-#{order_id}", raw: raw })
67
+ end
68
+
69
+ def get_order(symbol:, order_id: nil, client_order_id: nil)
70
+ result = get_signed("/openApi/spot/v1/trade/query", {
71
+ symbol: symbol, orderId: order_id, clientOrderID: client_order_id
72
+ })
73
+ return result if result.failure?
74
+ return api_error("BingX", result.data) unless result.data.is_a?(Hash) && result.data["code"].to_i.zero?
75
+
76
+ raw = result.data.key?("data") ? result.data["data"] : result.data
77
+ Result::Success.new(normalize_order("#{symbol}-#{raw['orderId']}", raw))
78
+ end
79
+
80
+ def cancel_order(symbol:, order_id: nil, client_order_id: nil)
81
+ post_signed("/openApi/spot/v1/trade/cancel", {
82
+ symbol: symbol, orderId: order_id, clientOrderID: client_order_id
83
+ })
84
+ end
85
+
86
+ def get_all_coins_info
87
+ get_signed("/openApi/wallets/v1/capital/config/getall")
88
+ end
89
+
90
+ def get_trade_fills(symbol: nil, order_id: nil, start_time: nil, end_time: nil, from_id: nil, limit: nil)
91
+ get_signed("/openApi/spot/v1/trade/fills", {
92
+ symbol: symbol, orderId: order_id,
93
+ startTime: start_time, endTime: end_time, fromId: from_id, limit: limit
94
+ })
95
+ end
96
+
97
+ def deposit_history(coin: nil, status: nil, start_time: nil, end_time: nil, offset: nil, limit: nil)
98
+ get_signed("/openApi/wallets/v1/capital/deposit/hisrec", {
99
+ coin: coin, status: status,
100
+ startTime: start_time, endTime: end_time, offset: offset, limit: limit
101
+ })
102
+ end
103
+
104
+ def withdraw_history(coin: nil, status: nil, start_time: nil, end_time: nil, offset: nil, limit: nil, id: nil)
105
+ get_signed("/openApi/wallets/v1/capital/withdraw/history", {
106
+ coin: coin, status: status,
107
+ startTime: start_time, endTime: end_time, offset: offset, limit: limit, id: id
108
+ })
109
+ end
110
+
111
+ def withdraw(coin:, address:, amount:, network: nil, wallet_type: nil, tag: nil)
112
+ post_signed("/openApi/wallets/v1/capital/withdraw/apply", {
113
+ coin: coin, address: address, amount: amount,
114
+ network: network, walletType: wallet_type, tag: tag
115
+ })
116
+ end
117
+
118
+ # --- Futures ---
119
+
120
+ def futures_income(symbol: nil, income_type: nil, start_time: nil, end_time: nil, limit: nil)
121
+ with_rescue do
122
+ params = {
123
+ symbol: symbol, incomeType: income_type,
124
+ startTime: start_time, endTime: end_time, limit: limit,
125
+ timestamp: timestamp_ms
126
+ }.compact
127
+ params[:signature] = hmac_sha256(@api_secret, Faraday::Utils.build_query(params))
128
+
129
+ response = futures_connection.get do |req|
130
+ req.url "/openApi/swap/v2/user/income"
131
+ req.headers = { "X-BX-APIKEY": @api_key }
132
+ req.params = params
133
+ end
134
+ response.body
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def normalize_order(order_id, raw)
141
+ order_type = parse_order_type(raw["type"])
142
+ side = raw["side"]&.downcase&.to_sym
143
+ status = parse_order_status(raw["status"])
144
+
145
+ amount = BigDecimal((raw["origQty"] || "0").to_s)
146
+ amount = nil if amount.zero?
147
+ quote_amount = raw["origQuoteOrderQty"] ? BigDecimal(raw["origQuoteOrderQty"].to_s) : nil
148
+ quote_amount = nil if quote_amount&.zero?
149
+
150
+ amount_exec = BigDecimal((raw["executedQty"] || "0").to_s)
151
+ quote_amount_exec = BigDecimal((raw["cummulativeQuoteQty"] || "0").to_s)
152
+ quote_amount_exec = nil if quote_amount_exec.negative?
153
+
154
+ price = BigDecimal((raw["price"] || "0").to_s)
155
+ if price.zero? && quote_amount_exec&.positive? && amount_exec.positive?
156
+ price = quote_amount_exec / amount_exec
157
+ end
158
+ price = nil if price.zero?
159
+
160
+ {
161
+ order_id: order_id, status: status, side: side, order_type: order_type,
162
+ price: price, amount: amount, quote_amount: quote_amount,
163
+ amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
164
+ }
165
+ end
166
+
167
+ def parse_order_type(type)
168
+ case type
169
+ when "MARKET" then :market
170
+ when "LIMIT" then :limit
171
+ else :unknown
172
+ end
173
+ end
174
+
175
+ def parse_order_status(status)
176
+ case status
177
+ when "NEW", "PARTIALLY_FILLED", "PENDING" then :open
178
+ when "FILLED" then :closed
179
+ when "CANCELED", "EXPIRED" then :cancelled
180
+ when "REJECTED", "FAILED" then :failed
181
+ else :unknown
182
+ end
183
+ end
184
+
185
+ def validate_trading_credentials
186
+ result = get_raw_balances
187
+ return Result::Failure.new("Invalid trading credentials") if result.failure?
188
+ result.data["code"]&.to_i&.zero? ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
189
+ end
190
+
191
+ def validate_read_credentials
192
+ validate_trading_credentials
193
+ end
194
+
195
+ def get_public(path, params = {})
196
+ with_rescue do
197
+ response = connection.get do |req|
198
+ req.url path
199
+ req.params = params.compact
200
+ end
201
+ response.body
202
+ end
203
+ end
204
+
205
+ def get_signed(path, params = {})
206
+ with_rescue do
207
+ params = params.compact.merge(timestamp: timestamp_ms)
208
+ params[:signature] = hmac_sha256(@api_secret, Faraday::Utils.build_query(params))
209
+
210
+ response = connection.get do |req|
211
+ req.url path
212
+ req.headers = { "X-BX-APIKEY": @api_key }
213
+ req.params = params
214
+ end
215
+ response.body
216
+ end
217
+ end
218
+
219
+ def post_signed(path, params = {})
220
+ with_rescue do
221
+ params = params.compact.merge(timestamp: timestamp_ms)
222
+ params[:signature] = hmac_sha256(@api_secret, Faraday::Utils.build_query(params))
223
+
224
+ response = connection.post do |req|
225
+ req.url path
226
+ req.headers = { "X-BX-APIKEY": @api_key, "Content-Type": "application/json" }
227
+ req.params = params
228
+ end
229
+ response.body
230
+ end
231
+ end
232
+
233
+ def futures_connection
234
+ @futures_connection ||= build_client_connection("https://open-api.bingx.com", content_type_match: //)
235
+ end
236
+
237
+ def connection
238
+ @connection ||= build_client_connection(URL, content_type_match: //)
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honeymaker
4
+ module Clients
5
+ class Bitget < Client
6
+ URL = "https://api.bitget.com"
7
+ RATE_LIMITS = { default: 100, orders: 200 }.freeze
8
+
9
+ attr_reader :passphrase
10
+
11
+ def initialize(api_key: nil, api_secret: nil, passphrase: nil, proxy: nil, logger: nil)
12
+ super(api_key: api_key, api_secret: api_secret, proxy: proxy, logger: logger)
13
+ @passphrase = passphrase
14
+ end
15
+
16
+ def get_coins
17
+ get_public("/api/v2/spot/public/coins")
18
+ end
19
+
20
+ def get_symbols
21
+ get_public("/api/v2/spot/public/symbols")
22
+ end
23
+
24
+ def get_tickers(symbol: nil)
25
+ get_public("/api/v2/spot/market/tickers", { symbol: symbol })
26
+ end
27
+
28
+ def get_orderbook(symbol:, limit: nil)
29
+ get_public("/api/v2/spot/market/orderbook", { symbol: symbol, limit: limit })
30
+ end
31
+
32
+ def get_candles(symbol:, granularity:, start_time: nil, end_time: nil, limit: nil)
33
+ get_public("/api/v2/spot/market/candles", {
34
+ symbol: symbol, granularity: granularity,
35
+ startTime: start_time, endTime: end_time, limit: limit
36
+ })
37
+ end
38
+
39
+ def get_account_assets(coin: nil)
40
+ get_signed("/api/v2/spot/account/assets", { coin: coin })
41
+ end
42
+
43
+ def get_balances
44
+ result = get_account_assets
45
+ return result if result.failure?
46
+
47
+ return api_error("Bitget", result.data) unless result.data["code"] == "00000"
48
+
49
+ balances = {}
50
+ (result.data["data"] || []).each do |asset|
51
+ symbol = asset["coin"]
52
+ free = BigDecimal((asset["available"] || "0").to_s)
53
+ locked = BigDecimal((asset["frozen"] || "0").to_s)
54
+ next if free.zero? && locked.zero?
55
+ balances[symbol] = { free: free, locked: locked }
56
+ end
57
+
58
+ Result::Success.new(balances)
59
+ end
60
+
61
+ def place_order(symbol:, side:, order_type:, size: nil, quote_size: nil, price: nil, force: nil, client_oid: nil)
62
+ result = post_signed("/api/v2/spot/trade/place-order", {
63
+ symbol: symbol, side: side, orderType: order_type,
64
+ size: size, quoteSize: quote_size, price: price, force: force, clientOid: client_oid
65
+ })
66
+ return result if result.failure?
67
+ return api_error("Bitget", result.data) unless result.data["code"] == "00000"
68
+
69
+ order_id = result.data.dig("data", "orderId")
70
+ Result::Success.new({ order_id: "#{symbol}-#{order_id}", raw: result.data })
71
+ end
72
+
73
+ def get_order(order_id: nil, client_oid: nil)
74
+ result = get_signed("/api/v2/spot/trade/orderInfo", { orderId: order_id, clientOid: client_oid })
75
+ return result if result.failure?
76
+ return api_error("Bitget", result.data) unless result.data["code"] == "00000"
77
+
78
+ order_list = result.data["data"]
79
+ raw = order_list.is_a?(Array) ? order_list.first : order_list
80
+ return Result::Failure.new("Order not found") unless raw
81
+
82
+ Result::Success.new(normalize_order(raw["orderId"] || order_id, raw))
83
+ end
84
+
85
+ def cancel_order(symbol:, order_id: nil, client_oid: nil)
86
+ post_signed("/api/v2/spot/trade/cancel-order", {
87
+ symbol: symbol, orderId: order_id, clientOid: client_oid
88
+ })
89
+ end
90
+
91
+ def get_fills(symbol: nil, order_id: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
92
+ get_signed("/api/v2/spot/trade/fills", {
93
+ symbol: symbol, orderId: order_id,
94
+ startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
95
+ })
96
+ end
97
+
98
+ def deposit_list(coin: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
99
+ get_signed("/api/v2/spot/wallet/deposit-records", {
100
+ coin: coin, startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
101
+ })
102
+ end
103
+
104
+ def withdrawal_list(coin: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
105
+ get_signed("/api/v2/spot/wallet/withdrawal-records", {
106
+ coin: coin, startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
107
+ })
108
+ end
109
+
110
+ def withdraw(coin:, address:, size:, transfer_type: nil, chain: nil, tag: nil, client_oid: nil)
111
+ post_signed("/api/v2/spot/wallet/withdrawal", {
112
+ coin: coin, transferType: transfer_type, address: address,
113
+ size: size, chain: chain, tag: tag, clientOid: client_oid
114
+ })
115
+ end
116
+
117
+ # --- Margin (Cross) ---
118
+
119
+ def margin_crossed_borrow_history(loan_id: nil, coin: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
120
+ get_signed("/api/v2/margin/crossed/borrow-history", {
121
+ loanId: loan_id, coin: coin,
122
+ startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
123
+ })
124
+ end
125
+
126
+ def margin_crossed_repay_history(repay_id: nil, coin: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
127
+ get_signed("/api/v2/margin/crossed/repay-history", {
128
+ repayId: repay_id, coin: coin,
129
+ startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
130
+ })
131
+ end
132
+
133
+ def margin_crossed_interest_history(coin: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
134
+ get_signed("/api/v2/margin/crossed/interest-history", {
135
+ coin: coin, startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
136
+ })
137
+ end
138
+
139
+ def margin_crossed_liquidation_history(start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
140
+ get_signed("/api/v2/margin/crossed/liquidation-history", {
141
+ startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
142
+ })
143
+ end
144
+
145
+ # --- Margin (Isolated) ---
146
+
147
+ def margin_isolated_borrow_history(symbol: nil, loan_id: nil, coin: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
148
+ get_signed("/api/v2/margin/isolated/borrow-history", {
149
+ symbol: symbol, loanId: loan_id, coin: coin,
150
+ startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
151
+ })
152
+ end
153
+
154
+ def margin_isolated_repay_history(symbol: nil, repay_id: nil, coin: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
155
+ get_signed("/api/v2/margin/isolated/repay-history", {
156
+ symbol: symbol, repayId: repay_id, coin: coin,
157
+ startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
158
+ })
159
+ end
160
+
161
+ def margin_isolated_interest_history(symbol: nil, coin: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
162
+ get_signed("/api/v2/margin/isolated/interest-history", {
163
+ symbol: symbol, coin: coin,
164
+ startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
165
+ })
166
+ end
167
+
168
+ def margin_isolated_liquidation_history(symbol: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
169
+ get_signed("/api/v2/margin/isolated/liquidation-history", {
170
+ symbol: symbol, startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
171
+ })
172
+ end
173
+
174
+ # --- Futures ---
175
+
176
+ def futures_account_bills(product_type:, coin: nil, business: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
177
+ get_signed("/api/v2/mix/account/bill", {
178
+ productType: product_type, coin: coin, business: business,
179
+ startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
180
+ })
181
+ end
182
+
183
+ def futures_fills_history(product_type:, symbol: nil, order_id: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
184
+ get_signed("/api/v2/mix/order/fills-history", {
185
+ productType: product_type, symbol: symbol, orderId: order_id,
186
+ startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
187
+ })
188
+ end
189
+
190
+ # --- Earn ---
191
+
192
+ def earn_savings_assets(coin: nil, filter: nil)
193
+ get_signed("/api/v2/earn/savings/assets", { coin: coin, filter: filter })
194
+ end
195
+
196
+ def earn_savings_subscribe_result(coin: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
197
+ get_signed("/api/v2/earn/savings/subscribe-result", {
198
+ coin: coin, startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
199
+ })
200
+ end
201
+
202
+ def earn_savings_redeem_result(coin: nil, start_time: nil, end_time: nil, limit: nil, id_less_than: nil)
203
+ get_signed("/api/v2/earn/savings/redeem-result", {
204
+ coin: coin, startTime: start_time, endTime: end_time, limit: limit, idLessThan: id_less_than
205
+ })
206
+ end
207
+
208
+ private
209
+
210
+ def normalize_order(order_id, raw)
211
+ order_type = parse_order_type(raw["orderType"])
212
+ side = raw["side"]&.downcase&.to_sym
213
+ status = parse_order_status(raw["status"])
214
+
215
+ price = BigDecimal((raw["priceAvg"] || raw["price"] || "0").to_s)
216
+ price = nil if price.zero?
217
+
218
+ amount = raw["size"] ? BigDecimal(raw["size"].to_s) : nil
219
+ quote_amount = raw["quoteSize"] ? BigDecimal(raw["quoteSize"].to_s) : nil
220
+ amount_exec = BigDecimal((raw["baseVolume"] || "0").to_s)
221
+ quote_amount_exec = BigDecimal((raw["quoteVolume"] || "0").to_s)
222
+
223
+ {
224
+ order_id: order_id, status: status, side: side, order_type: order_type,
225
+ price: price, amount: amount, quote_amount: quote_amount,
226
+ amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
227
+ }
228
+ end
229
+
230
+ def parse_order_type(type)
231
+ case type&.downcase
232
+ when "market" then :market
233
+ when "limit" then :limit
234
+ else :unknown
235
+ end
236
+ end
237
+
238
+ def parse_order_status(status)
239
+ case status
240
+ when "init", "new" then :unknown
241
+ when "partially_filled", "live" then :open
242
+ when "filled" then :closed
243
+ when "cancelled" then :cancelled
244
+ else :unknown
245
+ end
246
+ end
247
+
248
+ def validate_trading_credentials
249
+ result = get_account_assets
250
+ return Result::Failure.new("Invalid trading credentials") if result.failure?
251
+ result.data["code"] == "00000" ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
252
+ end
253
+
254
+ def validate_read_credentials
255
+ validate_trading_credentials
256
+ end
257
+
258
+ def get_public(path, params = {})
259
+ with_rescue do
260
+ response = connection.get do |req|
261
+ req.url path
262
+ req.headers = unauthenticated_headers
263
+ req.params = params.compact
264
+ end
265
+ response.body
266
+ end
267
+ end
268
+
269
+ def get_signed(path, params = {})
270
+ with_rescue do
271
+ params = params.compact
272
+ query_string = params.empty? ? "" : "?#{Faraday::Utils.build_query(params)}"
273
+ ts = timestamp_ms.to_s
274
+ pre_sign = "#{ts}GET#{path}#{query_string}"
275
+
276
+ response = connection.get do |req|
277
+ req.url path
278
+ req.headers = signed_headers(ts, pre_sign)
279
+ req.params = params
280
+ end
281
+ response.body
282
+ end
283
+ end
284
+
285
+ def post_signed(path, body = {})
286
+ with_rescue do
287
+ body = body.compact
288
+ ts = timestamp_ms.to_s
289
+ pre_sign = "#{ts}POST#{path}#{body.to_json}"
290
+
291
+ response = connection.post do |req|
292
+ req.url path
293
+ req.headers = signed_headers(ts, pre_sign)
294
+ req.body = body
295
+ end
296
+ response.body
297
+ end
298
+ end
299
+
300
+ def unauthenticated_headers
301
+ { Accept: "application/json", "Content-Type": "application/json" }
302
+ end
303
+
304
+ def signed_headers(timestamp, pre_sign)
305
+ mac = Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", @api_secret, pre_sign))
306
+ {
307
+ "ACCESS-KEY": @api_key,
308
+ "ACCESS-SIGN": mac,
309
+ "ACCESS-TIMESTAMP": timestamp,
310
+ "ACCESS-PASSPHRASE": @passphrase,
311
+ Accept: "application/json",
312
+ "Content-Type": "application/json",
313
+ locale: "en-US"
314
+ }
315
+ end
316
+ end
317
+ end
318
+ end