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
@@ -5,6 +5,7 @@ module Honeymaker
5
5
  class Bybit < Client
6
6
  URL = "https://api.bybit.com"
7
7
  RECV_WINDOW = "5000"
8
+ RATE_LIMITS = { default: 100, orders: 200 }.freeze
8
9
 
9
10
  def get_coin_query_info
10
11
  get_authenticated("/v5/asset/coin/query-info")
@@ -38,20 +39,54 @@ module Honeymaker
38
39
  get_authenticated("/v5/account/wallet-balance", { accountType: account_type, coin: coin })
39
40
  end
40
41
 
42
+ def get_balances(account_type: "UNIFIED")
43
+ result = wallet_balance(account_type: account_type)
44
+ return result if result.failure?
45
+
46
+ return Result::Failure.new(result.data["retMsg"]) unless result.data["retCode"]&.zero?
47
+
48
+ balances = {}
49
+ accounts = result.data.dig("result", "list") || []
50
+ accounts.each do |account|
51
+ (account["coin"] || []).each do |coin|
52
+ symbol = coin["coin"]
53
+ free = BigDecimal((coin["availableToWithdraw"] || "0").to_s)
54
+ locked = BigDecimal((coin["locked"] || "0").to_s)
55
+ next if free.zero? && locked.zero?
56
+ balances[symbol] = { free: free, locked: locked }
57
+ end
58
+ end
59
+
60
+ Result::Success.new(balances)
61
+ end
62
+
41
63
  def get_order(category:, order_id: nil, symbol: nil, order_link_id: nil)
42
- get_authenticated("/v5/order/realtime", {
64
+ result = get_authenticated("/v5/order/realtime", {
43
65
  category: category, orderId: order_id, symbol: symbol, orderLinkId: order_link_id
44
66
  })
67
+ return result if result.failure?
68
+ return Result::Failure.new(result.data["retMsg"]) unless result.data["retCode"]&.zero?
69
+
70
+ order_list = result.data.dig("result", "list") || []
71
+ raw = order_list.first
72
+ return Result::Failure.new("Order not found") unless raw
73
+
74
+ Result::Success.new(normalize_order(raw["orderId"], raw))
45
75
  end
46
76
 
47
77
  def create_order(category:, symbol:, side:, order_type:, qty:, price: nil,
48
78
  time_in_force: nil, market_unit: nil, order_link_id: nil)
49
- post_authenticated("/v5/order/create", {
79
+ result = post_authenticated("/v5/order/create", {
50
80
  category: category, symbol: symbol, side: side,
51
81
  orderType: order_type, qty: qty, price: price,
52
82
  timeInForce: time_in_force, marketUnit: market_unit,
53
83
  orderLinkId: order_link_id
54
84
  })
85
+ return result if result.failure?
86
+ return Result::Failure.new(result.data["retMsg"]) unless result.data["retCode"]&.zero?
87
+
88
+ order_id = result.data.dig("result", "orderId")
89
+ Result::Success.new({ order_id: order_id, raw: result.data })
55
90
  end
56
91
 
57
92
  def cancel_order(category:, symbol:, order_id: nil, order_link_id: nil)
@@ -95,8 +130,91 @@ module Honeymaker
95
130
  })
96
131
  end
97
132
 
133
+ # --- Margin ---
134
+
135
+ def borrow_history(currency: nil, start_time: nil, end_time: nil, limit: nil, cursor: nil)
136
+ get_authenticated("/v5/account/borrow-history", {
137
+ currency: currency, startTime: start_time, endTime: end_time, limit: limit, cursor: cursor
138
+ })
139
+ end
140
+
141
+ def spot_margin_repay_history(start_time: nil, end_time: nil, coin: nil, limit: nil, cursor: nil)
142
+ get_authenticated("/v5/spot-cross-margin-trade/repay-history", {
143
+ startTime: start_time, endTime: end_time, coin: coin, limit: limit, cursor: cursor
144
+ })
145
+ end
146
+
147
+ # --- Futures ---
148
+
149
+ def delivery_records(category:, symbol: nil, exp_date: nil, limit: nil, cursor: nil)
150
+ get_authenticated("/v5/asset/delivery-record", {
151
+ category: category, symbol: symbol, expDate: exp_date, limit: limit, cursor: cursor
152
+ })
153
+ end
154
+
155
+ def settlement_records(category:, symbol: nil, limit: nil, cursor: nil)
156
+ get_authenticated("/v5/asset/settlement-record", {
157
+ category: category, symbol: symbol, limit: limit, cursor: cursor
158
+ })
159
+ end
160
+
161
+ # --- Earn ---
162
+
163
+ def earn_order_records(order_id: nil, order_type: nil, start_time: nil, end_time: nil, limit: nil, cursor: nil)
164
+ get_authenticated("/v5/earn/order-records", {
165
+ orderId: order_id, orderType: order_type,
166
+ startTime: start_time, endTime: end_time, limit: limit, cursor: cursor
167
+ })
168
+ end
169
+
170
+ def earn_yield_history(product_id: nil, start_time: nil, end_time: nil, limit: nil, cursor: nil)
171
+ get_authenticated("/v5/earn/yield-history", {
172
+ productId: product_id, startTime: start_time, endTime: end_time, limit: limit, cursor: cursor
173
+ })
174
+ end
175
+
98
176
  private
99
177
 
178
+ def normalize_order(order_id, raw)
179
+ order_type = parse_order_type(raw["orderType"])
180
+ side = raw["side"]&.downcase&.to_sym
181
+ status = parse_order_status(raw["orderStatus"])
182
+
183
+ price = BigDecimal((raw["avgPrice"] || "0").to_s)
184
+ price = BigDecimal((raw["price"] || "0").to_s) if price.zero?
185
+ price = nil if price.zero?
186
+
187
+ amount = BigDecimal((raw["qty"] || "0").to_s)
188
+ amount = nil if amount.zero?
189
+ amount_exec = BigDecimal((raw["cumExecQty"] || "0").to_s)
190
+ quote_amount_exec = BigDecimal((raw["cumExecValue"] || "0").to_s)
191
+
192
+ {
193
+ order_id: order_id, status: status, side: side, order_type: order_type,
194
+ price: price, amount: amount, quote_amount: nil,
195
+ amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
196
+ }
197
+ end
198
+
199
+ def parse_order_type(type)
200
+ case type
201
+ when "Market" then :market
202
+ when "Limit" then :limit
203
+ else :unknown
204
+ end
205
+ end
206
+
207
+ def parse_order_status(status)
208
+ case status
209
+ when "Created", "Untriggered" then :unknown
210
+ when "New", "PartiallyFilled", "PartiallyFilledCanceled" then :open
211
+ when "Filled" then :closed
212
+ when "Cancelled", "Expired", "Deactivated" then :cancelled
213
+ when "Rejected" then :failed
214
+ else :unknown
215
+ end
216
+ end
217
+
100
218
  def validate_trading_credentials
101
219
  result = wallet_balance(account_type: "UNIFIED")
102
220
  return Result::Failure.new("Invalid trading credentials") if result.failure?
@@ -6,6 +6,7 @@ module Honeymaker
6
6
  module Clients
7
7
  class Coinbase < Client
8
8
  URL = "https://api.coinbase.com"
9
+ RATE_LIMITS = { default: 100, orders: 100 }.freeze
9
10
 
10
11
  def initialize(api_key: nil, api_secret: nil, proxy: nil, logger: nil)
11
12
  super
@@ -13,7 +14,11 @@ module Honeymaker
13
14
  end
14
15
 
15
16
  def get_order(order_id:)
16
- get("/api/v3/brokerage/orders/historical/#{order_id}")
17
+ result = get("/api/v3/brokerage/orders/historical/#{order_id}")
18
+ return result if result.failure?
19
+
20
+ raw = result.data.is_a?(Hash) && result.data.key?("order") ? result.data["order"] : result.data
21
+ Result::Success.new(normalize_order(order_id, raw))
17
22
  end
18
23
 
19
24
  def list_orders(order_ids: nil, product_ids: nil, product_type: nil, order_status: nil,
@@ -40,10 +45,20 @@ module Honeymaker
40
45
  end
41
46
 
42
47
  def create_order(client_order_id:, product_id:, side:, order_configuration:)
43
- post("/api/v3/brokerage/orders", {
48
+ result = post("/api/v3/brokerage/orders", {
44
49
  client_order_id: client_order_id, product_id: product_id,
45
50
  side: side, order_configuration: order_configuration
46
51
  })
52
+ return result if result.failure?
53
+
54
+ raw = result.data
55
+ if raw["success"] == false
56
+ error_msg = raw.dig("error_response", "message") || "Order creation failed"
57
+ return Result::Failure.new(error_msg)
58
+ end
59
+
60
+ order_id = raw.dig("success_response", "order_id")
61
+ Result::Success.new({ order_id: order_id, raw: raw })
47
62
  end
48
63
 
49
64
  def cancel_orders(order_ids:)
@@ -98,6 +113,35 @@ module Honeymaker
98
113
  get("/api/v3/brokerage/portfolios")
99
114
  end
100
115
 
116
+ def get_balances(portfolio_uuid: nil)
117
+ unless portfolio_uuid
118
+ portfolios_result = list_portfolios
119
+ return portfolios_result if portfolios_result.failure?
120
+
121
+ portfolio = Array(portfolios_result.data["portfolios"]).first
122
+ return Result::Failure.new("No portfolios found") unless portfolio
123
+ portfolio_uuid = portfolio["uuid"]
124
+ end
125
+
126
+ result = get_portfolio_breakdown(portfolio_uuid: portfolio_uuid)
127
+ return result if result.failure?
128
+
129
+ balances = {}
130
+ positions = result.data.dig("breakdown", "spot_positions") || []
131
+ positions.each do |position|
132
+ symbol = position["asset"]
133
+ next unless symbol
134
+
135
+ free = BigDecimal((position["available_to_trade_crypto"] || "0").to_s)
136
+ total = BigDecimal((position["total_balance_crypto"] || "0").to_s)
137
+ locked = total - free
138
+ next if free.zero? && locked.zero?
139
+ balances[symbol] = { free: free, locked: locked }
140
+ end
141
+
142
+ Result::Success.new(balances)
143
+ end
144
+
101
145
  def get_portfolio_breakdown(portfolio_uuid:, currency: nil)
102
146
  get("/api/v3/brokerage/portfolios/#{portfolio_uuid}", { currency: currency })
103
147
  end
@@ -154,6 +198,68 @@ module Honeymaker
154
198
  end
155
199
  end
156
200
 
201
+ def normalize_order(order_id, raw)
202
+ order_type = parse_order_type(raw["order_type"])
203
+ side = raw["side"]&.downcase&.to_sym
204
+ status = parse_order_status(raw["status"])
205
+
206
+ price = BigDecimal((raw["average_filled_price"] || "0").to_s)
207
+
208
+ case order_type
209
+ when :limit
210
+ config = (raw["order_configuration"] || {})["limit_limit_gtc"] || {}
211
+ amount = config["base_size"] ? BigDecimal(config["base_size"].to_s) : nil
212
+ quote_amount = config["quote_size"] ? BigDecimal(config["quote_size"].to_s) : nil
213
+ price = BigDecimal(config["limit_price"].to_s) if price.zero? && config["limit_price"]
214
+ when :market
215
+ config = (raw["order_configuration"] || {})["market_market_ioc"] || {}
216
+ amount = config["base_size"] ? BigDecimal(config["base_size"].to_s) : nil
217
+ quote_amount = config["quote_size"] ? BigDecimal(config["quote_size"].to_s) : nil
218
+ else
219
+ amount = nil
220
+ quote_amount = nil
221
+ end
222
+
223
+ price = nil if price.zero?
224
+
225
+ amount_exec = BigDecimal((raw["filled_size"] || "0").to_s)
226
+ total_value = BigDecimal((raw["total_value_after_fees"] || "0").to_s)
227
+ outstanding = BigDecimal((raw["outstanding_hold_amount"] || "0").to_s)
228
+ quote_amount_exec = total_value - outstanding
229
+
230
+ {
231
+ order_id: order_id,
232
+ status: status,
233
+ side: side,
234
+ order_type: order_type,
235
+ price: price,
236
+ amount: amount,
237
+ quote_amount: quote_amount,
238
+ amount_exec: amount_exec,
239
+ quote_amount_exec: quote_amount_exec,
240
+ raw: raw
241
+ }
242
+ end
243
+
244
+ def parse_order_type(type)
245
+ case type
246
+ when "MARKET" then :market
247
+ when "LIMIT" then :limit
248
+ else :unknown
249
+ end
250
+ end
251
+
252
+ def parse_order_status(status)
253
+ case status
254
+ when "PENDING", "UNKNOWN_ORDER_STATUS" then :unknown
255
+ when "OPEN" then :open
256
+ when "FILLED" then :closed
257
+ when "CANCELLED", "EXPIRED" then :cancelled
258
+ when "FAILED" then :failed
259
+ else :unknown
260
+ end
261
+ end
262
+
157
263
  def validate_trading_credentials
158
264
  # List accounts — if it works, the key has trading access
159
265
  result = list_accounts
@@ -4,6 +4,7 @@ module Honeymaker
4
4
  module Clients
5
5
  class Gemini < Client
6
6
  URL = "https://api.gemini.com"
7
+ RATE_LIMITS = { default: 200, orders: 200 }.freeze
7
8
 
8
9
  def get_symbols
9
10
  get_public("/v1/symbols")
@@ -21,20 +22,46 @@ module Honeymaker
21
22
  get_public("/v2/candles/#{symbol}/#{time_frame}")
22
23
  end
23
24
 
24
- def get_balances
25
+ def get_raw_balances
25
26
  post_signed("/v1/balances")
26
27
  end
27
28
 
29
+ def get_balances
30
+ result = get_raw_balances
31
+ return result if result.failure?
32
+
33
+ balances = {}
34
+ Array(result.data).each do |balance|
35
+ symbol = balance["currency"]&.upcase
36
+ next unless symbol
37
+ available = BigDecimal((balance["available"] || "0").to_s)
38
+ amount = BigDecimal((balance["amount"] || "0").to_s)
39
+ locked = amount - available
40
+ next if available.zero? && locked.zero?
41
+ balances[symbol] = { free: available, locked: locked }
42
+ end
43
+
44
+ Result::Success.new(balances)
45
+ end
46
+
28
47
  def new_order(symbol:, amount:, price:, side:, type:, client_order_id: nil, options: [])
29
- post_signed("/v1/order/new", {
48
+ result = post_signed("/v1/order/new", {
30
49
  symbol: symbol, amount: amount, price: price,
31
50
  side: side, type: type, client_order_id: client_order_id,
32
51
  options: options
33
52
  })
53
+ return result if result.failure?
54
+
55
+ raw = result.data
56
+ Result::Success.new({ order_id: raw["order_id"].to_s, raw: raw })
34
57
  end
35
58
 
36
59
  def order_status(order_id:)
37
- post_signed("/v1/order/status", { order_id: order_id })
60
+ result = post_signed("/v1/order/status", { order_id: order_id })
61
+ return result if result.failure?
62
+
63
+ raw = result.data
64
+ Result::Success.new(normalize_order(raw["order_id"].to_s, raw))
38
65
  end
39
66
 
40
67
  def cancel_order(order_id:)
@@ -53,10 +80,64 @@ module Honeymaker
53
80
  post_signed("/v1/withdraw/#{currency}", { address: address, amount: amount })
54
81
  end
55
82
 
83
+ # --- Staking ---
84
+
85
+ def staking_history
86
+ post_signed("/v1/staking/history")
87
+ end
88
+
89
+ def staking_rewards
90
+ post_signed("/v1/staking/rewards")
91
+ end
92
+
93
+ def staking_balances
94
+ post_signed("/v1/balances/staking")
95
+ end
96
+
56
97
  private
57
98
 
99
+ def normalize_order(order_id, raw)
100
+ order_type = parse_order_type(raw["type"])
101
+ side = raw["side"]&.downcase&.to_sym
102
+ status = parse_gemini_order_status(raw)
103
+
104
+ price = BigDecimal((raw["avg_execution_price"] || "0").to_s)
105
+ price = BigDecimal((raw["price"] || "0").to_s) if price.zero?
106
+ price = nil if price.zero?
107
+
108
+ amount = BigDecimal((raw["original_amount"] || "0").to_s)
109
+ amount_exec = BigDecimal((raw["executed_amount"] || "0").to_s)
110
+ quote_amount_exec = price ? (amount_exec * price) : BigDecimal("0")
111
+
112
+ {
113
+ order_id: order_id, status: status, side: side, order_type: order_type,
114
+ price: price, amount: amount, quote_amount: nil,
115
+ amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
116
+ }
117
+ end
118
+
119
+ def parse_order_type(type)
120
+ case type&.downcase
121
+ when "exchange limit", "limit" then :limit
122
+ when "market", "exchange market" then :market
123
+ else :limit
124
+ end
125
+ end
126
+
127
+ def parse_gemini_order_status(raw)
128
+ if raw["is_cancelled"]
129
+ :cancelled
130
+ elsif raw["is_live"] && BigDecimal((raw["remaining_amount"] || "0").to_s).positive?
131
+ :open
132
+ elsif !raw["is_live"] && BigDecimal((raw["executed_amount"] || "0").to_s).positive?
133
+ :closed
134
+ else
135
+ :unknown
136
+ end
137
+ end
138
+
58
139
  def validate_trading_credentials
59
- result = get_balances
140
+ result = get_raw_balances
60
141
  return Result::Failure.new("Invalid trading credentials") if result.failure?
61
142
  result.data.is_a?(Array) ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
62
143
  end
@@ -4,6 +4,7 @@ module Honeymaker
4
4
  module Clients
5
5
  class Hyperliquid < Client
6
6
  URL = "https://api.hyperliquid.xyz"
7
+ RATE_LIMITS = { default: 200, orders: 200 }.freeze
7
8
 
8
9
  def spot_meta
9
10
  post_info({ type: "spotMeta" })
@@ -17,8 +18,51 @@ module Honeymaker
17
18
  post_info({ type: "spotClearinghouseState", user: user })
18
19
  end
19
20
 
21
+ def get_balances(user: nil)
22
+ user ||= @api_key
23
+ result = spot_clearinghouse_state(user: user)
24
+ return result if result.failure?
25
+
26
+ balances = {}
27
+ (result.data["balances"] || []).each do |balance|
28
+ symbol = balance["coin"]
29
+ total = BigDecimal((balance["total"] || "0").to_s)
30
+ hold = BigDecimal((balance["hold"] || "0").to_s)
31
+ free = total - hold
32
+ next if free.zero? && hold.zero?
33
+ balances[symbol] = { free: free, locked: hold }
34
+ end
35
+
36
+ Result::Success.new(balances)
37
+ end
38
+
20
39
  def order_status(user:, oid:)
21
- post_info({ type: "orderStatus", user: user, oid: oid })
40
+ result = post_info({ type: "orderStatus", user: user, oid: oid })
41
+ return result if result.failure?
42
+
43
+ raw = result.data
44
+ order = raw["order"] || {}
45
+ fills = raw["fills"] || []
46
+ status_str = raw["status"]
47
+
48
+ coin = order["coin"]
49
+ side = order["side"] == "B" ? :buy : :sell
50
+ limit_price = BigDecimal((order["limitPx"] || "0").to_s)
51
+ ordered_size = BigDecimal((order["sz"] || "0").to_s)
52
+
53
+ amount_exec = fills.sum { |f| BigDecimal(f["sz"].to_s) }
54
+ quote_amount_exec = fills.sum { |f| BigDecimal(f["px"].to_s) * BigDecimal(f["sz"].to_s) }
55
+ avg_price = amount_exec.positive? ? (quote_amount_exec / amount_exec) : limit_price
56
+ avg_price = nil if avg_price.zero?
57
+
58
+ status = parse_order_status(status_str)
59
+
60
+ Result::Success.new({
61
+ order_id: "#{coin}-#{oid}",
62
+ status: status, side: side, order_type: :limit,
63
+ price: avg_price, amount: ordered_size, quote_amount: nil,
64
+ amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
65
+ })
22
66
  end
23
67
 
24
68
  def open_orders(user:)
@@ -38,8 +82,32 @@ module Honeymaker
38
82
  post_info(body)
39
83
  end
40
84
 
85
+ # --- Futures ---
86
+
87
+ def user_funding(user:, start_time:, end_time: nil)
88
+ body = { type: "userFunding", user: user, startTime: start_time }
89
+ body[:endTime] = end_time if end_time
90
+ post_info(body)
91
+ end
92
+
93
+ def user_non_funding_ledger_updates(user:, start_time:, end_time: nil)
94
+ body = { type: "userNonFundingLedgerUpdates", user: user, startTime: start_time }
95
+ body[:endTime] = end_time if end_time
96
+ post_info(body)
97
+ end
98
+
41
99
  private
42
100
 
101
+ def parse_order_status(status)
102
+ case status
103
+ when "open", "marginCanceled" then :open
104
+ when "filled" then :closed
105
+ when "canceled", "triggered", "rejected" then :cancelled
106
+ when "unknownOid" then :unknown
107
+ else :unknown
108
+ end
109
+ end
110
+
43
111
  def validate_trading_credentials
44
112
  return Result::Failure.new("No wallet address provided") unless @api_key
45
113
  result = open_orders(user: @api_key)
@@ -8,11 +8,30 @@ module Honeymaker
8
8
  class Kraken < Client
9
9
  URL = "https://api.kraken.com"
10
10
 
11
+ RATE_LIMITS = { default: 1000, orders: 1000 }.freeze
12
+
13
+ ASSET_MAP = {
14
+ "ZUSD" => "USD", "ZEUR" => "EUR", "ZGBP" => "GBP",
15
+ "ZJPY" => "JPY", "ZCHF" => "CHF", "ZCAD" => "CAD",
16
+ "ZAUD" => "AUD", "XXBT" => "XBT", "XETH" => "ETH",
17
+ "XXDG" => "XDG"
18
+ }.freeze
19
+
11
20
  def query_orders_info(txid:, trades: nil, userref: nil, consolidate_taker: true)
12
- post_private("/0/private/QueryOrders", {
21
+ result = post_private("/0/private/QueryOrders", {
13
22
  nonce: nonce, trades: trades, userref: userref,
14
23
  txid: txid, consolidate_taker: consolidate_taker
15
24
  })
25
+ return result if result.failure?
26
+
27
+ errors = result.data["error"]
28
+ return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
29
+
30
+ orders = {}
31
+ (result.data["result"] || {}).each do |order_id, raw|
32
+ orders[order_id] = normalize_order(order_id, raw)
33
+ end
34
+ Result::Success.new(orders)
16
35
  end
17
36
 
18
37
  def add_order(ordertype:, type:, volume:, pair:, userref: nil, cl_ord_id: nil,
@@ -20,7 +39,7 @@ module Honeymaker
20
39
  reduce_only: nil, stptype: nil, oflags: [], timeinforce: nil,
21
40
  starttm: nil, expiretm: nil, close: nil, close_price: nil,
22
41
  close_price2: nil, deadline: nil, validate: nil)
23
- post_private("/0/private/AddOrder", {
42
+ result = post_private("/0/private/AddOrder", {
24
43
  "nonce" => nonce, "ordertype" => ordertype, "type" => type,
25
44
  "volume" => volume, "pair" => pair, "userref" => userref,
26
45
  "cl_ord_id" => cl_ord_id, "displayvol" => displayvol,
@@ -33,6 +52,13 @@ module Honeymaker
33
52
  "close[price]" => close_price, "close[price2]" => close_price2,
34
53
  "deadline" => deadline, "validate" => validate
35
54
  })
55
+ return result if result.failure?
56
+
57
+ errors = result.data["error"]
58
+ return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
59
+
60
+ txid = (result.data.dig("result", "txid") || []).first
61
+ Result::Success.new({ order_id: txid, raw: result.data })
36
62
  end
37
63
 
38
64
  def cancel_order(txid: nil, cl_ord_id: nil)
@@ -57,6 +83,26 @@ module Honeymaker
57
83
  post_private("/0/private/BalanceEx", { nonce: nonce })
58
84
  end
59
85
 
86
+ def get_balances
87
+ result = get_extended_balance
88
+ return result if result.failure?
89
+
90
+ errors = result.data["error"]
91
+ return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
92
+
93
+ balances = {}
94
+ (result.data["result"] || {}).each do |symbol, balance|
95
+ mapped_symbol = ASSET_MAP[symbol.split(".").first] || symbol.split(".").first
96
+ total = BigDecimal(balance["balance"].to_s)
97
+ locked = BigDecimal((balance["hold_trade"] || "0").to_s)
98
+ free = total - locked
99
+ next if free.zero? && locked.zero?
100
+ balances[mapped_symbol] = { free: free, locked: locked }
101
+ end
102
+
103
+ Result::Success.new(balances)
104
+ end
105
+
60
106
  def get_ohlc_data(pair:, interval: nil, since: nil)
61
107
  get_public("/0/public/OHLC", { pair: pair, interval: interval, since: since })
62
108
  end
@@ -87,8 +133,72 @@ module Honeymaker
87
133
  post_private("/0/private/Withdraw", { nonce: nonce, asset: asset, key: key, amount: amount, address: address })
88
134
  end
89
135
 
136
+ # --- Earn ---
137
+
138
+ def get_earn_allocations(ascending: nil, converted_asset: nil, hide_zero_allocations: nil)
139
+ post_private("/0/private/Earn/Allocations", {
140
+ nonce: nonce, ascending: ascending,
141
+ converted_asset: converted_asset,
142
+ hide_zero_allocations: hide_zero_allocations
143
+ })
144
+ end
145
+
90
146
  private
91
147
 
148
+ def normalize_order(order_id, raw)
149
+ descr = raw["descr"] || {}
150
+ order_type = parse_order_type(descr["ordertype"])
151
+ side = descr["type"]&.downcase&.to_sym
152
+ status = parse_order_status(raw["status"])
153
+
154
+ order_flags = (raw["oflags"] || "").split(",")
155
+ if order_flags.include?("viqc")
156
+ amount = nil
157
+ quote_amount = BigDecimal(raw["vol"].to_s)
158
+ else
159
+ amount = BigDecimal(raw["vol"].to_s)
160
+ quote_amount = nil
161
+ end
162
+
163
+ amount_exec = BigDecimal(raw["vol_exec"].to_s)
164
+ quote_amount_exec = BigDecimal(raw["cost"].to_s)
165
+
166
+ price = BigDecimal(raw["price"].to_s)
167
+ price = BigDecimal(descr["price"].to_s) if price.zero? && order_type == :limit
168
+ price = nil if price.zero?
169
+
170
+ {
171
+ order_id: order_id,
172
+ status: status,
173
+ side: side,
174
+ order_type: order_type,
175
+ price: price,
176
+ amount: amount,
177
+ quote_amount: quote_amount,
178
+ amount_exec: amount_exec,
179
+ quote_amount_exec: quote_amount_exec,
180
+ raw: raw
181
+ }
182
+ end
183
+
184
+ def parse_order_type(type)
185
+ case type
186
+ when "market" then :market
187
+ when "limit" then :limit
188
+ else :unknown
189
+ end
190
+ end
191
+
192
+ def parse_order_status(status)
193
+ case status
194
+ when "pending" then :unknown
195
+ when "open" then :open
196
+ when "closed" then :closed
197
+ when "canceled", "expired" then :cancelled
198
+ else :unknown
199
+ end
200
+ end
201
+
92
202
  def validate_trading_credentials
93
203
  result = get_extended_balance
94
204
  return Result::Failure.new("Invalid trading credentials") if result.failure?