honeymaker 0.4.0 → 0.5.0

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 +100 -5
  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 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")
@@ -5,6 +5,7 @@ module Honeymaker
5
5
  class Bitvavo < Client
6
6
  URL = "https://api.bitvavo.com"
7
7
  ACCESS_WINDOW = "10000"
8
+ RATE_LIMITS = { default: 100, orders: 100 }.freeze
8
9
 
9
10
  def get_assets
10
11
  get_public("/v2/assets")
@@ -28,21 +29,46 @@ module Honeymaker
28
29
  })
29
30
  end
30
31
 
31
- def get_balance(symbol: nil)
32
+ def get_raw_balance(symbol: nil)
32
33
  get_signed("/v2/balance", { symbol: symbol })
33
34
  end
34
35
 
36
+ def get_balances
37
+ result = get_raw_balance
38
+ return result if result.failure?
39
+
40
+ balances = {}
41
+ Array(result.data).each do |balance|
42
+ symbol = balance["symbol"]
43
+ next unless symbol
44
+ free = BigDecimal((balance["available"] || "0").to_s)
45
+ locked = BigDecimal((balance["inOrder"] || "0").to_s)
46
+ next if free.zero? && locked.zero?
47
+ balances[symbol] = { free: free, locked: locked }
48
+ end
49
+
50
+ Result::Success.new(balances)
51
+ end
52
+
35
53
  def place_order(market:, side:, order_type:, amount: nil, amount_quote: nil, price: nil,
36
54
  time_in_force: nil, client_order_id: nil)
37
- post_signed("/v2/order", {
55
+ result = post_signed("/v2/order", {
38
56
  market: market, side: side, orderType: order_type,
39
57
  amount: amount, amountQuote: amount_quote, price: price,
40
58
  timeInForce: time_in_force, clientOrderId: client_order_id
41
59
  })
60
+ return result if result.failure?
61
+
62
+ raw = result.data
63
+ Result::Success.new({ order_id: "#{raw['market']}-#{raw['orderId']}", raw: raw })
42
64
  end
43
65
 
44
66
  def get_order(market:, order_id:)
45
- get_signed("/v2/order", { market: market, orderId: order_id })
67
+ result = get_signed("/v2/order", { market: market, orderId: order_id })
68
+ return result if result.failure?
69
+
70
+ raw = result.data
71
+ Result::Success.new(normalize_order("#{raw['market']}-#{raw['orderId']}", raw))
46
72
  end
47
73
 
48
74
  def cancel_order(market:, order_id:)
@@ -70,10 +96,60 @@ module Honeymaker
70
96
  })
71
97
  end
72
98
 
99
+ # --- History ---
100
+
101
+ def get_transactions(limit: nil, start_time: nil, end_time: nil)
102
+ get_signed("/v2/transactions", { limit: limit, start: start_time, end: end_time })
103
+ end
104
+
73
105
  private
74
106
 
107
+ def normalize_order(order_id, raw)
108
+ order_type = parse_order_type(raw["orderType"])
109
+ side = raw["side"]&.downcase&.to_sym
110
+ status = parse_order_status(raw["status"])
111
+
112
+ price = BigDecimal((raw["price"] || "0").to_s)
113
+ amount = BigDecimal((raw["amount"] || "0").to_s)
114
+ amount = nil if amount.zero?
115
+ amount_quote = BigDecimal((raw["amountQuote"] || "0").to_s)
116
+ amount_quote = nil if amount_quote.zero?
117
+
118
+ amount_exec = BigDecimal((raw["filledAmount"] || "0").to_s)
119
+ quote_amount_exec = BigDecimal((raw["filledAmountQuote"] || "0").to_s)
120
+
121
+ if price.zero? && quote_amount_exec.positive? && amount_exec.positive?
122
+ price = quote_amount_exec / amount_exec
123
+ end
124
+ price = nil if price.zero?
125
+
126
+ {
127
+ order_id: order_id, status: status, side: side, order_type: order_type,
128
+ price: price, amount: amount, quote_amount: amount_quote,
129
+ amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
130
+ }
131
+ end
132
+
133
+ def parse_order_type(type)
134
+ case type
135
+ when "market" then :market
136
+ when "limit" then :limit
137
+ else :unknown
138
+ end
139
+ end
140
+
141
+ def parse_order_status(status)
142
+ case status
143
+ when "new", "partiallyFilled" then :open
144
+ when "filled" then :closed
145
+ when "canceled", "cancelled", "expired" then :cancelled
146
+ when "rejected" then :failed
147
+ else :unknown
148
+ end
149
+ end
150
+
75
151
  def validate_trading_credentials
76
- result = get_balance
152
+ result = get_raw_balance
77
153
  result.success? ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
78
154
  end
79
155