honeymaker 0.4.0 → 0.5.1

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +145 -6
  3. data/lib/honeymaker/client.rb +14 -0
  4. data/lib/honeymaker/clients/binance.rb +264 -2
  5. data/lib/honeymaker/clients/binance_us.rb +33 -0
  6. data/lib/honeymaker/clients/bingx.rb +100 -4
  7. data/lib/honeymaker/clients/bitget.rb +163 -2
  8. data/lib/honeymaker/clients/bitmart.rb +108 -2
  9. data/lib/honeymaker/clients/bitrue.rb +90 -2
  10. data/lib/honeymaker/clients/bitvavo.rb +80 -4
  11. data/lib/honeymaker/clients/bybit.rb +120 -2
  12. data/lib/honeymaker/clients/coinbase.rb +108 -2
  13. data/lib/honeymaker/clients/gemini.rb +85 -4
  14. data/lib/honeymaker/clients/hyperliquid.rb +69 -1
  15. data/lib/honeymaker/clients/kraken.rb +112 -2
  16. data/lib/honeymaker/clients/kraken_futures.rb +78 -0
  17. data/lib/honeymaker/clients/kucoin.rb +120 -2
  18. data/lib/honeymaker/clients/mexc.rb +85 -2
  19. data/lib/honeymaker/version.rb +1 -1
  20. data/lib/honeymaker.rb +3 -1
  21. data/test/honeymaker/clients/binance_client_test.rb +9 -2
  22. data/test/honeymaker/clients/bitget_client_test.rb +9 -3
  23. data/test/honeymaker/clients/bitmart_client_test.rb +7 -2
  24. data/test/honeymaker/clients/bitvavo_client_test.rb +2 -2
  25. data/test/honeymaker/clients/bybit_client_test.rb +7 -3
  26. data/test/honeymaker/clients/coinbase_client_test.rb +10 -3
  27. data/test/honeymaker/clients/honeymaker_client_registry_test.rb +1 -1
  28. data/test/honeymaker/clients/kraken_client_test.rb +2 -1
  29. data/test/honeymaker/clients/kraken_futures_client_test.rb +54 -0
  30. data/test/honeymaker/clients/kucoin_client_test.rb +8 -2
  31. data/test/honeymaker/clients/mexc_client_test.rb +6 -1
  32. metadata +17 -1
@@ -4,6 +4,7 @@ module Honeymaker
4
4
  module Clients
5
5
  class BingX < Client
6
6
  URL = "https://open-api.bingx.com"
7
+ RATE_LIMITS = { default: 100, orders: 200 }.freeze
7
8
 
8
9
  def get_symbols
9
10
  get_public("/openApi/spot/v1/common/symbols")
@@ -24,24 +25,50 @@ module Honeymaker
24
25
  })
25
26
  end
26
27
 
27
- def get_balances
28
+ def get_raw_balances
28
29
  get_signed("/openApi/spot/v1/account/balance")
29
30
  end
30
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
+
31
49
  def place_order(symbol:, side:, type:, quantity: nil, quote_order_qty: nil, price: nil,
32
50
  time_in_force: nil, client_order_id: nil)
33
- post_signed("/openApi/spot/v1/trade/order", {
51
+ result = post_signed("/openApi/spot/v1/trade/order", {
34
52
  symbol: symbol, side: side, type: type,
35
53
  quantity: quantity, quoteOrderQty: quote_order_qty,
36
54
  price: price, timeInForce: time_in_force,
37
55
  newClientOrderId: client_order_id
38
56
  })
57
+ return result if result.failure?
58
+
59
+ raw = result.data
60
+ order_id = raw.dig("data", "orderId") || raw.dig("data", "data", "orderId")
61
+ Result::Success.new({ order_id: "#{symbol}-#{order_id}", raw: raw })
39
62
  end
40
63
 
41
64
  def get_order(symbol:, order_id: nil, client_order_id: nil)
42
- get_signed("/openApi/spot/v1/trade/query", {
65
+ result = get_signed("/openApi/spot/v1/trade/query", {
43
66
  symbol: symbol, orderId: order_id, clientOrderID: client_order_id
44
67
  })
68
+ return result if result.failure?
69
+
70
+ raw = result.data.is_a?(Hash) && result.data.key?("data") ? result.data["data"] : result.data
71
+ Result::Success.new(normalize_order("#{symbol}-#{raw['orderId']}", raw))
45
72
  end
46
73
 
47
74
  def cancel_order(symbol:, order_id: nil, client_order_id: nil)
@@ -78,10 +105,75 @@ module Honeymaker
78
105
  })
79
106
  end
80
107
 
108
+ # --- Futures ---
109
+
110
+ def futures_income(symbol: nil, income_type: nil, start_time: nil, end_time: nil, limit: nil)
111
+ with_rescue do
112
+ params = {
113
+ symbol: symbol, incomeType: income_type,
114
+ startTime: start_time, endTime: end_time, limit: limit,
115
+ timestamp: timestamp_ms
116
+ }.compact
117
+ params[:signature] = hmac_sha256(@api_secret, Faraday::Utils.build_query(params))
118
+
119
+ response = futures_connection.get do |req|
120
+ req.url "/openApi/swap/v2/user/income"
121
+ req.headers = { "X-BX-APIKEY": @api_key }
122
+ req.params = params
123
+ end
124
+ response.body
125
+ end
126
+ end
127
+
81
128
  private
82
129
 
130
+ def normalize_order(order_id, raw)
131
+ order_type = parse_order_type(raw["type"])
132
+ side = raw["side"]&.downcase&.to_sym
133
+ status = parse_order_status(raw["status"])
134
+
135
+ amount = BigDecimal((raw["origQty"] || "0").to_s)
136
+ amount = nil if amount.zero?
137
+ quote_amount = raw["origQuoteOrderQty"] ? BigDecimal(raw["origQuoteOrderQty"].to_s) : nil
138
+ quote_amount = nil if quote_amount&.zero?
139
+
140
+ amount_exec = BigDecimal((raw["executedQty"] || "0").to_s)
141
+ quote_amount_exec = BigDecimal((raw["cummulativeQuoteQty"] || "0").to_s)
142
+ quote_amount_exec = nil if quote_amount_exec.negative?
143
+
144
+ price = BigDecimal((raw["price"] || "0").to_s)
145
+ if price.zero? && quote_amount_exec&.positive? && amount_exec.positive?
146
+ price = quote_amount_exec / amount_exec
147
+ end
148
+ price = nil if price.zero?
149
+
150
+ {
151
+ order_id: order_id, status: status, side: side, order_type: order_type,
152
+ price: price, amount: amount, quote_amount: quote_amount,
153
+ amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
154
+ }
155
+ end
156
+
157
+ def parse_order_type(type)
158
+ case type
159
+ when "MARKET" then :market
160
+ when "LIMIT" then :limit
161
+ else :unknown
162
+ end
163
+ end
164
+
165
+ def parse_order_status(status)
166
+ case status
167
+ when "NEW", "PARTIALLY_FILLED", "PENDING" then :open
168
+ when "FILLED" then :closed
169
+ when "CANCELED", "EXPIRED" then :cancelled
170
+ when "REJECTED", "FAILED" then :failed
171
+ else :unknown
172
+ end
173
+ end
174
+
83
175
  def validate_trading_credentials
84
- result = get_balances
176
+ result = get_raw_balances
85
177
  return Result::Failure.new("Invalid trading credentials") if result.failure?
86
178
  result.data["code"]&.to_i&.zero? ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
87
179
  end
@@ -128,6 +220,10 @@ module Honeymaker
128
220
  end
129
221
  end
130
222
 
223
+ def futures_connection
224
+ @futures_connection ||= build_client_connection("https://open-api.bingx.com", content_type_match: //)
225
+ end
226
+
131
227
  def connection
132
228
  @connection ||= build_client_connection(URL, content_type_match: //)
133
229
  end
@@ -4,6 +4,7 @@ module Honeymaker
4
4
  module Clients
5
5
  class Bitget < Client
6
6
  URL = "https://api.bitget.com"
7
+ RATE_LIMITS = { default: 100, orders: 200 }.freeze
7
8
 
8
9
  attr_reader :passphrase
9
10
 
@@ -39,15 +40,46 @@ module Honeymaker
39
40
  get_signed("/api/v2/spot/account/assets", { coin: coin })
40
41
  end
41
42
 
43
+ def get_balances
44
+ result = get_account_assets
45
+ return result if result.failure?
46
+
47
+ return Result::Failure.new("Bitget API error") 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
+
42
61
  def place_order(symbol:, side:, order_type:, size: nil, quote_size: nil, price: nil, force: nil, client_oid: nil)
43
- post_signed("/api/v2/spot/trade/place-order", {
62
+ result = post_signed("/api/v2/spot/trade/place-order", {
44
63
  symbol: symbol, side: side, orderType: order_type,
45
64
  size: size, quoteSize: quote_size, price: price, force: force, clientOid: client_oid
46
65
  })
66
+ return result if result.failure?
67
+ return Result::Failure.new("Bitget API error") unless result.data["code"] == "00000"
68
+
69
+ order_id = result.data.dig("data", "orderId")
70
+ Result::Success.new({ order_id: order_id, raw: result.data })
47
71
  end
48
72
 
49
73
  def get_order(order_id: nil, client_oid: nil)
50
- get_signed("/api/v2/spot/trade/orderInfo", { orderId: order_id, clientOid: client_oid })
74
+ result = get_signed("/api/v2/spot/trade/orderInfo", { orderId: order_id, clientOid: client_oid })
75
+ return result if result.failure?
76
+ return Result::Failure.new("Bitget API error") 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))
51
83
  end
52
84
 
53
85
  def cancel_order(symbol:, order_id: nil, client_oid: nil)
@@ -82,8 +114,137 @@ module Honeymaker
82
114
  })
83
115
  end
84
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
+
85
208
  private
86
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 "partial_fill", "live" then :open
242
+ when "full_fill" then :closed
243
+ when "cancelled" then :cancelled
244
+ else :unknown
245
+ end
246
+ end
247
+
87
248
  def validate_trading_credentials
88
249
  result = get_account_assets
89
250
  return Result::Failure.new("Invalid trading credentials") if result.failure?
@@ -4,6 +4,7 @@ module Honeymaker
4
4
  module Clients
5
5
  class BitMart < Client
6
6
  URL = "https://api-cloud.bitmart.com"
7
+ RATE_LIMITS = { default: 100, orders: 200 }.freeze
7
8
 
8
9
  attr_reader :memo
9
10
 
@@ -34,16 +35,46 @@ module Honeymaker
34
35
  get_signed("/spot/v1/wallet")
35
36
  end
36
37
 
38
+ def get_balances
39
+ result = get_wallet
40
+ return result if result.failure?
41
+
42
+ return Result::Failure.new("BitMart API error") unless result.data["code"] == 1000
43
+
44
+ balances = {}
45
+ (result.data.dig("data", "wallet") || []).each do |wallet|
46
+ symbol = wallet["id"]
47
+ free = BigDecimal((wallet["available"] || "0").to_s)
48
+ locked = BigDecimal((wallet["frozen"] || "0").to_s)
49
+ next if free.zero? && locked.zero?
50
+ balances[symbol] = { free: free, locked: locked }
51
+ end
52
+
53
+ Result::Success.new(balances)
54
+ end
55
+
37
56
  def submit_order(symbol:, side:, type:, size: nil, notional: nil, price: nil, client_order_id: nil)
38
- post_signed("/spot/v2/submit_order", {
57
+ result = post_signed("/spot/v2/submit_order", {
39
58
  symbol: symbol, side: side, type: type,
40
59
  size: size, notional: notional, price: price,
41
60
  client_order_id: client_order_id
42
61
  })
62
+ return result if result.failure?
63
+ return Result::Failure.new("BitMart API error") unless result.data["code"] == 1000
64
+
65
+ order_id = result.data.dig("data", "order_id")
66
+ Result::Success.new({ order_id: order_id.to_s, raw: result.data })
43
67
  end
44
68
 
45
69
  def get_order(order_id:)
46
- post_signed("/spot/v2/order_detail", { orderId: order_id })
70
+ result = post_signed("/spot/v2/order_detail", { orderId: order_id })
71
+ return result if result.failure?
72
+ return Result::Failure.new("BitMart API error") unless result.data["code"] == 1000
73
+
74
+ raw = result.data["data"]
75
+ return Result::Failure.new("Order not found") unless raw
76
+
77
+ Result::Success.new(normalize_order(order_id.to_s, raw))
47
78
  end
48
79
 
49
80
  def cancel_order(symbol:, order_id: nil, client_order_id: nil)
@@ -79,8 +110,83 @@ module Honeymaker
79
110
  })
80
111
  end
81
112
 
113
+ # --- Margin ---
114
+
115
+ def margin_borrow_records(symbol: nil, borrow_id: nil, start_time: nil, end_time: nil, n: nil)
116
+ get_signed("/spot/v1/margin/isolated/borrow_record", {
117
+ symbol: symbol, borrow_id: borrow_id, start_time: start_time, end_time: end_time, N: n
118
+ })
119
+ end
120
+
121
+ def margin_repay_records(symbol: nil, repay_id: nil, currency: nil, start_time: nil, end_time: nil, n: nil)
122
+ get_signed("/spot/v1/margin/isolated/repay_record", {
123
+ symbol: symbol, repay_id: repay_id, currency: currency, start_time: start_time, end_time: end_time, N: n
124
+ })
125
+ end
126
+
127
+ # --- Futures ---
128
+
129
+ def futures_transaction_history(start_time: nil, end_time: nil, page_num: nil, page_size: nil, flow_type: nil)
130
+ get_signed("/contract/private/transaction-history", {
131
+ start_time: start_time, end_time: end_time,
132
+ page_num: page_num, page_size: page_size, flow_type: flow_type
133
+ })
134
+ end
135
+
136
+ def futures_trades(symbol:, start_time: nil, end_time: nil, page_num: nil, page_size: nil)
137
+ get_signed("/contract/private/trades", {
138
+ symbol: symbol, start_time: start_time, end_time: end_time,
139
+ page_num: page_num, page_size: page_size
140
+ })
141
+ end
142
+
82
143
  private
83
144
 
145
+ def normalize_order(order_id, raw)
146
+ order_type = parse_order_type(raw["type"])
147
+ side = raw["side"]&.downcase&.to_sym
148
+ status = parse_order_status(raw["status"])
149
+
150
+ amount = BigDecimal((raw["size"] || "0").to_s)
151
+ amount = nil if amount.zero?
152
+ quote_amount = raw["notional"] ? BigDecimal(raw["notional"].to_s) : nil
153
+ quote_amount = nil if quote_amount&.zero?
154
+
155
+ amount_exec = BigDecimal((raw["filled_size"] || "0").to_s)
156
+ quote_amount_exec = BigDecimal((raw["filled_notional"] || "0").to_s)
157
+ quote_amount_exec = nil if quote_amount_exec.negative?
158
+
159
+ price = BigDecimal((raw["price"] || "0").to_s)
160
+ if price.zero? && quote_amount_exec&.positive? && amount_exec.positive?
161
+ price = quote_amount_exec / amount_exec
162
+ end
163
+ price = nil if price.zero?
164
+
165
+ {
166
+ order_id: order_id, status: status, side: side, order_type: order_type,
167
+ price: price, amount: amount, quote_amount: quote_amount,
168
+ amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
169
+ }
170
+ end
171
+
172
+ def parse_order_type(type)
173
+ case type
174
+ when "market" then :market
175
+ when "limit" then :limit
176
+ else :unknown
177
+ end
178
+ end
179
+
180
+ def parse_order_status(status)
181
+ case status
182
+ when "new", "partially_filled" then :open
183
+ when "filled" then :closed
184
+ when "canceled", "expired", "partially_canceled" then :cancelled
185
+ when "rejected", "failed" then :failed
186
+ else :unknown
187
+ end
188
+ end
189
+
84
190
  def validate_trading_credentials
85
191
  result = get_wallet
86
192
  return Result::Failure.new("Invalid trading credentials") if result.failure?
@@ -4,6 +4,7 @@ module Honeymaker
4
4
  module Clients
5
5
  class Bitrue < Client
6
6
  URL = "https://openapi.bitrue.com"
7
+ RATE_LIMITS = { default: 100, orders: 200 }.freeze
7
8
 
8
9
  def exchange_information
9
10
  get_public("/api/v1/exchangeInfo")
@@ -28,21 +29,45 @@ module Honeymaker
28
29
  get_signed("/api/v1/account", { recvWindow: recv_window })
29
30
  end
30
31
 
32
+ def get_balances
33
+ result = account_information
34
+ return result if result.failure?
35
+
36
+ balances = {}
37
+ Array(result.data["balances"]).each do |balance|
38
+ symbol = balance["asset"]
39
+ free = BigDecimal(balance["free"].to_s)
40
+ locked = BigDecimal(balance["locked"].to_s)
41
+ next if free.zero? && locked.zero?
42
+ balances[symbol] = { free: free, locked: locked }
43
+ end
44
+
45
+ Result::Success.new(balances)
46
+ end
47
+
31
48
  def query_order(symbol:, order_id: nil, orig_client_order_id: nil, recv_window: 5000)
32
- get_signed("/api/v1/order", {
49
+ result = get_signed("/api/v1/order", {
33
50
  symbol: symbol, orderId: order_id,
34
51
  origClientOrderId: orig_client_order_id, recvWindow: recv_window
35
52
  })
53
+ return result if result.failure?
54
+
55
+ raw = result.data
56
+ Result::Success.new(normalize_order("#{symbol}-#{raw['orderId']}", raw))
36
57
  end
37
58
 
38
59
  def new_order(symbol:, side:, type:, time_in_force: nil, quantity: nil, quote_order_qty: nil,
39
60
  price: nil, new_client_order_id: nil, recv_window: 5000)
40
- post_signed("/api/v1/order", {
61
+ result = post_signed("/api/v1/order", {
41
62
  symbol: symbol, side: side, type: type,
42
63
  timeInForce: time_in_force, quantity: quantity,
43
64
  quoteOrderQty: quote_order_qty, price: price,
44
65
  newClientOrderId: new_client_order_id, recvWindow: recv_window
45
66
  })
67
+ return result if result.failure?
68
+
69
+ raw = result.data
70
+ Result::Success.new({ order_id: "#{symbol}-#{raw['orderId']}", raw: raw })
46
71
  end
47
72
 
48
73
  def cancel_order(symbol:, order_id: nil, orig_client_order_id: nil, recv_window: 5000)
@@ -59,8 +84,71 @@ module Honeymaker
59
84
  })
60
85
  end
61
86
 
87
+ # --- Futures ---
88
+
89
+ def futures_account(recv_window: 5000)
90
+ with_rescue do
91
+ response = futures_connection.get do |req|
92
+ req.url "/fapi/v1/account"
93
+ req.headers = auth_headers
94
+ req.params = { recvWindow: recv_window, timestamp: timestamp_ms }.compact
95
+ req.params[:signature] = sign_params(req.params)
96
+ end
97
+ response.body
98
+ end
99
+ end
100
+
62
101
  private
63
102
 
103
+ def futures_connection
104
+ @futures_connection ||= build_client_connection("https://fapi.bitrue.com")
105
+ end
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_order_status(raw["status"])
111
+
112
+ amount = BigDecimal((raw["origQty"] || "0").to_s)
113
+ amount = nil if amount.zero?
114
+ quote_amount = raw["origQuoteOrderQty"] ? BigDecimal(raw["origQuoteOrderQty"].to_s) : nil
115
+ quote_amount = nil if quote_amount&.zero?
116
+
117
+ amount_exec = BigDecimal((raw["executedQty"] || "0").to_s)
118
+ quote_amount_exec = BigDecimal((raw["cummulativeQuoteQty"] || "0").to_s)
119
+ quote_amount_exec = nil if quote_amount_exec.negative?
120
+
121
+ price = BigDecimal((raw["price"] || "0").to_s)
122
+ if price.zero? && quote_amount_exec&.positive? && amount_exec.positive?
123
+ price = quote_amount_exec / amount_exec
124
+ end
125
+ price = nil if price.zero?
126
+
127
+ {
128
+ order_id: order_id, status: status, side: side, order_type: order_type,
129
+ price: price, amount: amount, quote_amount: quote_amount,
130
+ amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
131
+ }
132
+ end
133
+
134
+ def parse_order_type(type)
135
+ case type
136
+ when "MARKET" then :market
137
+ when "LIMIT" then :limit
138
+ else :unknown
139
+ end
140
+ end
141
+
142
+ def parse_order_status(status)
143
+ case status
144
+ when "NEW", "PARTIALLY_FILLED" then :open
145
+ when "FILLED" then :closed
146
+ when "CANCELED", "EXPIRED" then :cancelled
147
+ when "REJECTED" then :failed
148
+ else :unknown
149
+ end
150
+ end
151
+
64
152
  def validate_trading_credentials
65
153
  result = account_information
66
154
  result.success? ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")