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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b69b5d2b1995f1def30eaa09cb003cef99a8763ea383c9f71ea8aa0d05e9ecac
4
- data.tar.gz: 54fb293018729bcfd12c26b596da9fae540bfe6574d9de40cae8b6b0407809bb
3
+ metadata.gz: 3263fa7b4c91808f48416df3570c42d21948759fb5190b9fe523671da953ce29
4
+ data.tar.gz: ffb885474e57533e933cbeb36d81f6c63586e4b84282322757ebfaef3037b046
5
5
  SHA512:
6
- metadata.gz: e1903ef50d96bd65cc630b587a2e4d9f8e7e967bd82768fd2c5d883dd06d8e265364fb853949e0f5308a4ba0dfc65980c91c96d9c89f5d1632966dbf756d9d35
7
- data.tar.gz: cc74f411106c324c6a12a9b93f02415cc1f14d65d38ee4518b313e487804a87c50c386fb7d9fea0716f21018d46fc42ce82ff9c3496552723c1dfb774318679b
6
+ metadata.gz: 4c76739afb0c6239ae6af0e2292646c43aba16a6ef1abf004ad1df65b9a305a6ca47976517ae5ca01ffa3ca6e1b3889882315ae33e15ef695b2f97b711764f51
7
+ data.tar.gz: 4c4c7b51f95757f0ef330c54abdc215dd01bc6dc5242de1c2367b5cbe80188207715b7ab6534cd682b86cf27bc7fed62ae4016bcc46ba94fc89c2b7b88df8104
data/README.md CHANGED
@@ -18,24 +18,119 @@ gem "honeymaker"
18
18
 
19
19
  ## Usage
20
20
 
21
+ ### Market Data
22
+
21
23
  ```ruby
22
24
  require "honeymaker"
23
25
 
24
- # Get an exchange client
25
26
  exchange = Honeymaker.exchange("binance")
26
-
27
- # Fetch trading pair info (symbols, decimals, min/max amounts)
28
27
  result = exchange.get_tickers_info
29
28
 
30
29
  if result.success?
31
30
  result.data.each do |ticker|
32
31
  puts "#{ticker[:ticker]} — min: #{ticker[:minimum_quote_size]}, decimals: #{ticker[:base_decimals]}"
33
32
  end
34
- else
35
- puts "Error: #{result.errors.join(', ')}"
36
33
  end
37
34
  ```
38
35
 
36
+ ### Balances
37
+
38
+ ```ruby
39
+ client = Honeymaker.client("binance", api_key: "...", api_secret: "...")
40
+ result = client.get_balances
41
+
42
+ if result.success?
43
+ result.data.each do |symbol, balance|
44
+ puts "#{symbol}: free=#{balance[:free]}, locked=#{balance[:locked]}"
45
+ end
46
+ end
47
+ # => { "BTC" => { free: BigDecimal("0.5"), locked: BigDecimal("0.1") }, ... }
48
+ ```
49
+
50
+ Coinbase auto-resolves the default portfolio, or pass one explicitly:
51
+
52
+ ```ruby
53
+ client.get_balances(portfolio_uuid: "...")
54
+ ```
55
+
56
+ ### Placing Orders
57
+
58
+ Order placement returns a normalized `{ order_id:, raw: }` hash:
59
+
60
+ ```ruby
61
+ client = Honeymaker.client("binance", api_key: "...", api_secret: "...")
62
+
63
+ result = client.new_order(symbol: "BTCUSDT", side: "BUY", type: "MARKET", quote_order_qty: "100")
64
+ if result.success?
65
+ puts result.data[:order_id] # => "BTCUSDT-123456"
66
+ puts result.data[:raw] # full exchange response
67
+ end
68
+ ```
69
+
70
+ Method names vary by exchange (`new_order`, `create_order`, `add_order`, `place_order`, `submit_order`) but the return format is the same.
71
+
72
+ ### Querying Orders
73
+
74
+ Order queries return a normalized hash with unified status, amounts, and the raw response:
75
+
76
+ ```ruby
77
+ result = client.query_order(symbol: "BTCUSDT", order_id: 123456)
78
+ if result.success?
79
+ order = result.data
80
+ order[:order_id] # => "BTCUSDT-123456"
81
+ order[:status] # => :open, :closed, :cancelled, :failed, :unknown
82
+ order[:side] # => :buy, :sell
83
+ order[:order_type] # => :market, :limit
84
+ order[:price] # => BigDecimal — avg fill price
85
+ order[:amount] # => BigDecimal — requested base qty (nil if quote-denominated)
86
+ order[:quote_amount] # => BigDecimal — requested quote qty (nil if base-denominated)
87
+ order[:amount_exec] # => BigDecimal — filled base qty
88
+ order[:quote_amount_exec] # => BigDecimal — filled quote qty
89
+ order[:raw] # => Hash — full exchange response
90
+ end
91
+ ```
92
+
93
+ ### Credential Validation
94
+
95
+ ```ruby
96
+ client = Honeymaker.client("binance", api_key: "...", api_secret: "...")
97
+ result = client.validate(:trading)
98
+ result.success? # => true if credentials have trading permissions
99
+ ```
100
+
101
+ ### Rate Limits
102
+
103
+ Each exchange exposes rate limit metadata (milliseconds between requests):
104
+
105
+ ```ruby
106
+ Honeymaker::Clients::Binance.rate_limits
107
+ # => { default: 100, orders: 200 }
108
+
109
+ Honeymaker::Clients::Kraken.rate_limits
110
+ # => { default: 1000, orders: 1000 }
111
+ ```
112
+
113
+ ### Proxy Support
114
+
115
+ ```ruby
116
+ client = Honeymaker.client("binance",
117
+ api_key: "...", api_secret: "...",
118
+ proxy: "http://proxy:8100"
119
+ )
120
+ ```
121
+
122
+ ## Result Objects
123
+
124
+ All methods return `Result::Success` or `Result::Failure`:
125
+
126
+ ```ruby
127
+ result = client.get_balances
128
+ result.success? # true/false
129
+ result.failure? # true/false
130
+ result.data # response payload
131
+ result.errors # array of error messages (empty on success)
132
+ ```
133
+
39
134
  ## License
40
135
 
41
136
  MIT
@@ -3,6 +3,7 @@
3
3
  require "openssl"
4
4
  require "base64"
5
5
  require "securerandom"
6
+ require "bigdecimal"
6
7
 
7
8
  module Honeymaker
8
9
  class Client
@@ -14,6 +15,11 @@ module Honeymaker
14
15
  }
15
16
  }.freeze
16
17
 
18
+ RATE_LIMITS = {
19
+ default: 100,
20
+ orders: 100
21
+ }.freeze
22
+
17
23
  attr_reader :api_key, :api_secret
18
24
 
19
25
  def initialize(api_key: nil, api_secret: nil, proxy: nil, logger: nil)
@@ -23,6 +29,14 @@ module Honeymaker
23
29
  @logger = logger
24
30
  end
25
31
 
32
+ def self.rate_limits
33
+ self::RATE_LIMITS
34
+ end
35
+
36
+ def get_balances
37
+ raise NotImplementedError, "#{self.class} must implement #get_balances"
38
+ end
39
+
26
40
  def validate(type = :trading)
27
41
  return Result::Failure.new("No credentials provided") unless authenticated?
28
42
 
@@ -4,6 +4,7 @@ module Honeymaker
4
4
  module Clients
5
5
  class Binance < Client
6
6
  URL = "https://api.binance.com"
7
+ RATE_LIMITS = { default: 100, orders: 200 }.freeze
7
8
 
8
9
  def exchange_information(symbol: nil, symbols: nil, permissions: nil, show_permission_sets: nil, symbol_status: nil)
9
10
  with_rescue do
@@ -75,6 +76,22 @@ module Honeymaker
75
76
  end
76
77
  end
77
78
 
79
+ def get_balances
80
+ result = account_information(omit_zero_balances: true)
81
+ return result if result.failure?
82
+
83
+ balances = {}
84
+ Array(result.data["balances"]).each do |balance|
85
+ symbol = balance["asset"]
86
+ free = BigDecimal(balance["free"].to_s)
87
+ locked = BigDecimal(balance["locked"].to_s)
88
+ next if free.zero? && locked.zero?
89
+ balances[symbol] = { free: free, locked: locked }
90
+ end
91
+
92
+ Result::Success.new(balances)
93
+ end
94
+
78
95
  def account_trade_list(symbol:, order_id: nil, start_time: nil, end_time: nil, from_id: nil, limit: 500, recv_window: 5000)
79
96
  with_rescue do
80
97
  response = connection.get do |req|
@@ -104,7 +121,8 @@ module Honeymaker
104
121
  }.compact
105
122
  req.params[:signature] = sign_params(req.params)
106
123
  end
107
- response.body
124
+ raw = response.body
125
+ normalize_order("#{symbol}-#{raw['orderId']}", raw)
108
126
  end
109
127
  end
110
128
 
@@ -145,7 +163,8 @@ module Honeymaker
145
163
  }.compact
146
164
  req.params[:signature] = sign_params(req.params)
147
165
  end
148
- response.body
166
+ raw = response.body
167
+ { order_id: "#{symbol}-#{raw['orderId']}", raw: raw }
149
168
  end
150
169
  end
151
170
 
@@ -363,8 +382,251 @@ module Honeymaker
363
382
  end
364
383
  end
365
384
 
385
+ # --- Margin ---
386
+
387
+ def margin_borrow_repay_history(type:, asset: nil, isolated_symbol: nil, start_time: nil, end_time: nil,
388
+ current: nil, size: nil, recv_window: 5000)
389
+ with_rescue do
390
+ response = connection.get do |req|
391
+ req.url "/sapi/v1/margin/borrow-repay"
392
+ req.headers = headers
393
+ req.params = {
394
+ type: type, asset: asset, isolatedSymbol: isolated_symbol,
395
+ startTime: start_time, endTime: end_time,
396
+ current: current, size: size,
397
+ recvWindow: recv_window, timestamp: timestamp_ms
398
+ }.compact
399
+ req.params[:signature] = sign_params(req.params)
400
+ end
401
+ response.body
402
+ end
403
+ end
404
+
405
+ def margin_interest_history(asset: nil, isolated_symbol: nil, start_time: nil, end_time: nil,
406
+ current: nil, size: nil, archived: nil, recv_window: 5000)
407
+ with_rescue do
408
+ response = connection.get do |req|
409
+ req.url "/sapi/v1/margin/interestHistory"
410
+ req.headers = headers
411
+ req.params = {
412
+ asset: asset, isolatedSymbol: isolated_symbol,
413
+ startTime: start_time, endTime: end_time,
414
+ current: current, size: size, archived: archived,
415
+ recvWindow: recv_window, timestamp: timestamp_ms
416
+ }.compact
417
+ req.params[:signature] = sign_params(req.params)
418
+ end
419
+ response.body
420
+ end
421
+ end
422
+
423
+ def margin_force_liquidation(start_time: nil, end_time: nil, isolated_symbol: nil,
424
+ current: nil, size: nil, recv_window: 5000)
425
+ with_rescue do
426
+ response = connection.get do |req|
427
+ req.url "/sapi/v1/margin/forceLiquidationRec"
428
+ req.headers = headers
429
+ req.params = {
430
+ startTime: start_time, endTime: end_time,
431
+ isolatedSymbol: isolated_symbol,
432
+ current: current, size: size,
433
+ recvWindow: recv_window, timestamp: timestamp_ms
434
+ }.compact
435
+ req.params[:signature] = sign_params(req.params)
436
+ end
437
+ response.body
438
+ end
439
+ end
440
+
441
+ # --- Futures ---
442
+
443
+ def futures_income_history(symbol: nil, income_type: nil, start_time: nil, end_time: nil,
444
+ page: nil, limit: 1000, recv_window: 5000)
445
+ with_rescue do
446
+ response = usdt_futures_connection.get do |req|
447
+ req.url "/fapi/v1/income"
448
+ req.headers = headers
449
+ req.params = {
450
+ symbol: symbol, incomeType: income_type,
451
+ startTime: start_time, endTime: end_time,
452
+ page: page, limit: limit,
453
+ recvWindow: recv_window, timestamp: timestamp_ms
454
+ }.compact
455
+ req.params[:signature] = sign_params(req.params)
456
+ end
457
+ response.body
458
+ end
459
+ end
460
+
461
+ def coin_futures_income_history(symbol: nil, income_type: nil, start_time: nil, end_time: nil,
462
+ page: nil, limit: 1000, recv_window: 5000)
463
+ with_rescue do
464
+ response = coin_futures_connection.get do |req|
465
+ req.url "/dapi/v1/income"
466
+ req.headers = headers
467
+ req.params = {
468
+ symbol: symbol, incomeType: income_type,
469
+ startTime: start_time, endTime: end_time,
470
+ page: page, limit: limit,
471
+ recvWindow: recv_window, timestamp: timestamp_ms
472
+ }.compact
473
+ req.params[:signature] = sign_params(req.params)
474
+ end
475
+ response.body
476
+ end
477
+ end
478
+
479
+ # --- Simple Earn ---
480
+
481
+ def simple_earn_flexible_subscriptions(product_id: nil, purchase_id: nil, asset: nil,
482
+ start_time: nil, end_time: nil, current: nil, size: nil)
483
+ with_rescue do
484
+ response = connection.get do |req|
485
+ req.url "/sapi/v1/simple-earn/flexible/history/subscriptionRecord"
486
+ req.headers = headers
487
+ req.params = {
488
+ productId: product_id, purchaseId: purchase_id, asset: asset,
489
+ startTime: start_time, endTime: end_time,
490
+ current: current, size: size, timestamp: timestamp_ms
491
+ }.compact
492
+ req.params[:signature] = sign_params(req.params)
493
+ end
494
+ response.body
495
+ end
496
+ end
497
+
498
+ def simple_earn_flexible_redemptions(product_id: nil, redeem_id: nil, asset: nil,
499
+ start_time: nil, end_time: nil, current: nil, size: nil)
500
+ with_rescue do
501
+ response = connection.get do |req|
502
+ req.url "/sapi/v1/simple-earn/flexible/history/redemptionRecord"
503
+ req.headers = headers
504
+ req.params = {
505
+ productId: product_id, redeemId: redeem_id, asset: asset,
506
+ startTime: start_time, endTime: end_time,
507
+ current: current, size: size, timestamp: timestamp_ms
508
+ }.compact
509
+ req.params[:signature] = sign_params(req.params)
510
+ end
511
+ response.body
512
+ end
513
+ end
514
+
515
+ def simple_earn_locked_subscriptions(product_id: nil, purchase_id: nil, asset: nil,
516
+ start_time: nil, end_time: nil, current: nil, size: nil)
517
+ with_rescue do
518
+ response = connection.get do |req|
519
+ req.url "/sapi/v1/simple-earn/locked/history/subscriptionRecord"
520
+ req.headers = headers
521
+ req.params = {
522
+ productId: product_id, purchaseId: purchase_id, asset: asset,
523
+ startTime: start_time, endTime: end_time,
524
+ current: current, size: size, timestamp: timestamp_ms
525
+ }.compact
526
+ req.params[:signature] = sign_params(req.params)
527
+ end
528
+ response.body
529
+ end
530
+ end
531
+
532
+ def simple_earn_locked_redemptions(product_id: nil, redeem_id: nil, asset: nil,
533
+ start_time: nil, end_time: nil, current: nil, size: nil)
534
+ with_rescue do
535
+ response = connection.get do |req|
536
+ req.url "/sapi/v1/simple-earn/locked/history/redemptionRecord"
537
+ req.headers = headers
538
+ req.params = {
539
+ productId: product_id, redeemId: redeem_id, asset: asset,
540
+ startTime: start_time, endTime: end_time,
541
+ current: current, size: size, timestamp: timestamp_ms
542
+ }.compact
543
+ req.params[:signature] = sign_params(req.params)
544
+ end
545
+ response.body
546
+ end
547
+ end
548
+
549
+ # --- Transfers ---
550
+
551
+ def universal_transfer_history(type:, start_time: nil, end_time: nil, current: nil, size: nil, recv_window: 5000)
552
+ with_rescue do
553
+ response = connection.get do |req|
554
+ req.url "/sapi/v1/asset/transfer"
555
+ req.headers = headers
556
+ req.params = {
557
+ type: type, startTime: start_time, endTime: end_time,
558
+ current: current, size: size,
559
+ recvWindow: recv_window, timestamp: timestamp_ms
560
+ }.compact
561
+ req.params[:signature] = sign_params(req.params)
562
+ end
563
+ response.body
564
+ end
565
+ end
566
+
366
567
  private
367
568
 
569
+ def usdt_futures_connection
570
+ @usdt_futures_connection ||= build_client_connection("https://fapi.binance.com")
571
+ end
572
+
573
+ def coin_futures_connection
574
+ @coin_futures_connection ||= build_client_connection("https://dapi.binance.com")
575
+ end
576
+
577
+ def normalize_order(order_id, raw)
578
+ order_type = parse_order_type(raw["type"])
579
+ side = raw["side"]&.downcase&.to_sym
580
+ status = parse_order_status(raw["status"])
581
+
582
+ amount = BigDecimal(raw["origQty"].to_s)
583
+ amount = nil if amount.zero?
584
+ quote_amount = BigDecimal(raw["origQuoteOrderQty"].to_s)
585
+ quote_amount = nil if quote_amount.zero?
586
+
587
+ amount_exec = BigDecimal(raw["executedQty"].to_s)
588
+ quote_amount_exec = BigDecimal(raw["cummulativeQuoteQty"].to_s)
589
+ quote_amount_exec = nil if quote_amount_exec.negative?
590
+
591
+ price = BigDecimal(raw["price"].to_s)
592
+ if price.zero? && quote_amount_exec&.positive? && amount_exec.positive?
593
+ price = quote_amount_exec / amount_exec
594
+ end
595
+ price = nil if price.zero?
596
+
597
+ {
598
+ order_id: order_id,
599
+ status: status,
600
+ side: side,
601
+ order_type: order_type,
602
+ price: price,
603
+ amount: amount,
604
+ quote_amount: quote_amount,
605
+ amount_exec: amount_exec,
606
+ quote_amount_exec: quote_amount_exec,
607
+ raw: raw
608
+ }
609
+ end
610
+
611
+ def parse_order_type(type)
612
+ case type
613
+ when "MARKET" then :market
614
+ when "LIMIT" then :limit
615
+ else :unknown
616
+ end
617
+ end
618
+
619
+ def parse_order_status(status)
620
+ case status
621
+ when "PENDING_CANCEL" then :unknown
622
+ when "NEW", "PENDING_NEW", "PARTIALLY_FILLED" then :open
623
+ when "FILLED" then :closed
624
+ when "CANCELED", "EXPIRED", "EXPIRED_IN_MATCH" then :cancelled
625
+ when "REJECTED" then :failed
626
+ else :unknown
627
+ end
628
+ end
629
+
368
630
  def validate_trading_credentials
369
631
  # Try cancelling a non-existent order — error -2011 (ORDER_DOES_NOT_EXIST) means key is valid with trade permission
370
632
  result = cancel_order(symbol: "ETHBTC", order_id: "9999999999")
@@ -4,6 +4,39 @@ module Honeymaker
4
4
  module Clients
5
5
  class BinanceUs < Binance
6
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
7
40
  end
8
41
  end
9
42
  end
@@ -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