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.
- checksums.yaml +4 -4
- data/README.md +100 -5
- data/lib/honeymaker/client.rb +14 -0
- data/lib/honeymaker/clients/binance.rb +264 -2
- data/lib/honeymaker/clients/binance_us.rb +33 -0
- data/lib/honeymaker/clients/bingx.rb +100 -4
- data/lib/honeymaker/clients/bitget.rb +163 -2
- data/lib/honeymaker/clients/bitmart.rb +108 -2
- data/lib/honeymaker/clients/bitrue.rb +90 -2
- data/lib/honeymaker/clients/bitvavo.rb +80 -4
- data/lib/honeymaker/clients/bybit.rb +120 -2
- data/lib/honeymaker/clients/coinbase.rb +108 -2
- data/lib/honeymaker/clients/gemini.rb +85 -4
- data/lib/honeymaker/clients/hyperliquid.rb +69 -1
- data/lib/honeymaker/clients/kraken.rb +112 -2
- data/lib/honeymaker/clients/kraken_futures.rb +78 -0
- data/lib/honeymaker/clients/kucoin.rb +120 -2
- data/lib/honeymaker/clients/mexc.rb +85 -2
- data/lib/honeymaker/version.rb +1 -1
- data/lib/honeymaker.rb +3 -1
- data/test/honeymaker/clients/binance_client_test.rb +9 -2
- data/test/honeymaker/clients/bitget_client_test.rb +9 -3
- data/test/honeymaker/clients/bitmart_client_test.rb +7 -2
- data/test/honeymaker/clients/bitvavo_client_test.rb +2 -2
- data/test/honeymaker/clients/bybit_client_test.rb +7 -3
- data/test/honeymaker/clients/coinbase_client_test.rb +10 -3
- data/test/honeymaker/clients/honeymaker_client_registry_test.rb +1 -1
- data/test/honeymaker/clients/kraken_client_test.rb +2 -1
- data/test/honeymaker/clients/kraken_futures_client_test.rb +54 -0
- data/test/honeymaker/clients/kucoin_client_test.rb +8 -2
- data/test/honeymaker/clients/mexc_client_test.rb +6 -1
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3263fa7b4c91808f48416df3570c42d21948759fb5190b9fe523671da953ce29
|
|
4
|
+
data.tar.gz: ffb885474e57533e933cbeb36d81f6c63586e4b84282322757ebfaef3037b046
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/honeymaker/client.rb
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|