honeymaker 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +145 -6
- 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
|
@@ -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
|
|
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 =
|
|
152
|
+
result = get_raw_balance
|
|
77
153
|
result.success? ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
|
|
78
154
|
end
|
|
79
155
|
|
|
@@ -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
|
|
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 =
|
|
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)
|