honeymaker 0.12.4-aarch64-linux
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 +7 -0
- data/.github/workflows/native-gems.yml +100 -0
- data/.github/workflows/release.yml +36 -0
- data/.github/workflows/test.yml +33 -0
- data/LICENSE +21 -0
- data/README.md +219 -0
- data/Rakefile +78 -0
- data/crates/honeymaker/tests/vectors/decimal.json +146 -0
- data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
- data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
- data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
- data/lib/honeymaker/3.4/honeymaker_native.so +0 -0
- data/lib/honeymaker/4.0/honeymaker_native.so +0 -0
- data/lib/honeymaker/client.rb +139 -0
- data/lib/honeymaker/clients/binance.rb +687 -0
- data/lib/honeymaker/clients/binance_us.rb +42 -0
- data/lib/honeymaker/clients/bingx.rb +242 -0
- data/lib/honeymaker/clients/bitget.rb +318 -0
- data/lib/honeymaker/clients/bitrue.rb +245 -0
- data/lib/honeymaker/clients/bitvavo.rb +244 -0
- data/lib/honeymaker/clients/bybit.rb +308 -0
- data/lib/honeymaker/clients/coinbase.rb +326 -0
- data/lib/honeymaker/clients/gemini.rb +189 -0
- data/lib/honeymaker/clients/hyperliquid.rb +232 -0
- data/lib/honeymaker/clients/kraken.rb +379 -0
- data/lib/honeymaker/clients/kraken_futures.rb +78 -0
- data/lib/honeymaker/clients/kucoin.rb +310 -0
- data/lib/honeymaker/clients/mexc.rb +249 -0
- data/lib/honeymaker/exchange.rb +129 -0
- data/lib/honeymaker/exchanges/binance.rb +55 -0
- data/lib/honeymaker/exchanges/binance_us.rb +9 -0
- data/lib/honeymaker/exchanges/bingx.rb +56 -0
- data/lib/honeymaker/exchanges/bitget.rb +52 -0
- data/lib/honeymaker/exchanges/bitrue.rb +53 -0
- data/lib/honeymaker/exchanges/bitvavo.rb +65 -0
- data/lib/honeymaker/exchanges/bybit.rb +57 -0
- data/lib/honeymaker/exchanges/coinbase.rb +69 -0
- data/lib/honeymaker/exchanges/gemini.rb +134 -0
- data/lib/honeymaker/exchanges/hyperliquid.rb +67 -0
- data/lib/honeymaker/exchanges/kraken.rb +122 -0
- data/lib/honeymaker/exchanges/kucoin.rb +52 -0
- data/lib/honeymaker/exchanges/mexc.rb +56 -0
- data/lib/honeymaker/native/kraken.rb +263 -0
- data/lib/honeymaker/native.rb +42 -0
- data/lib/honeymaker/result.rb +46 -0
- data/lib/honeymaker/utils.rb +39 -0
- data/lib/honeymaker/version.rb +5 -0
- data/lib/honeymaker.rb +97 -0
- data/script/parity/kraken_client_live.rb +191 -0
- data/script/parity/kraken_live.rb +70 -0
- data/script/parity/kraken_transport.rb +604 -0
- data/script/parity/resolve.c +49 -0
- data/script/release/push_gems.sh +28 -0
- data/script/vectors/kraken.rb +50 -0
- data/script/vectors/kraken_stage2.rb +146 -0
- data/sig/honeymaker.rbs +4 -0
- data/test/fixtures/binance_book_ticker.json +7 -0
- data/test/fixtures/binance_exchange_info.json +30 -0
- data/test/fixtures/bingx_symbols.json +26 -0
- data/test/fixtures/bingx_ticker.json +12 -0
- data/test/fixtures/bitget_symbols.json +28 -0
- data/test/fixtures/bitget_tickers.json +14 -0
- data/test/fixtures/bitmart_symbols.json +26 -0
- data/test/fixtures/bitmart_ticker.json +12 -0
- data/test/fixtures/bitrue_book_ticker.json +7 -0
- data/test/fixtures/bitrue_exchange_info.json +34 -0
- data/test/fixtures/bitvavo_book_ticker.json +7 -0
- data/test/fixtures/bitvavo_markets.json +30 -0
- data/test/fixtures/bybit_instruments.json +23 -0
- data/test/fixtures/bybit_tickers.json +19 -0
- data/test/fixtures/coinbase_product.json +7 -0
- data/test/fixtures/coinbase_products.json +24 -0
- data/test/fixtures/gemini_pubticker.json +10 -0
- data/test/fixtures/gemini_symbol_detail.json +10 -0
- data/test/fixtures/gemini_symbols.json +1 -0
- data/test/fixtures/hyperliquid_all_mids.json +5 -0
- data/test/fixtures/hyperliquid_spot_meta.json +12 -0
- data/test/fixtures/kraken_asset_pairs.json +43 -0
- data/test/fixtures/kraken_ticker.json +16 -0
- data/test/fixtures/kucoin_orderbook_level1.json +10 -0
- data/test/fixtures/kucoin_symbols.json +17 -0
- data/test/fixtures/mexc_book_ticker.json +7 -0
- data/test/fixtures/mexc_exchange_info.json +64 -0
- data/test/honeymaker/client_test.rb +90 -0
- data/test/honeymaker/clients/binance_client_test.rb +151 -0
- data/test/honeymaker/clients/binance_us_client_test.rb +25 -0
- data/test/honeymaker/clients/bingx_client_test.rb +106 -0
- data/test/honeymaker/clients/bitget_client_test.rb +146 -0
- data/test/honeymaker/clients/bitrue_client_test.rb +69 -0
- data/test/honeymaker/clients/bitvavo_client_test.rb +156 -0
- data/test/honeymaker/clients/bybit_client_test.rb +137 -0
- data/test/honeymaker/clients/coinbase_client_test.rb +153 -0
- data/test/honeymaker/clients/gemini_client_test.rb +100 -0
- data/test/honeymaker/clients/honeymaker_client_registry_test.rb +39 -0
- data/test/honeymaker/clients/hyperliquid_client_test.rb +292 -0
- data/test/honeymaker/clients/kraken_client_test.rb +255 -0
- data/test/honeymaker/clients/kraken_futures_client_test.rb +54 -0
- data/test/honeymaker/clients/kucoin_client_test.rb +149 -0
- data/test/honeymaker/clients/mexc_client_test.rb +98 -0
- data/test/honeymaker/clients/validation_test.rb +167 -0
- data/test/honeymaker/error_chain_test.rb +133 -0
- data/test/honeymaker/exchange_test.rb +138 -0
- data/test/honeymaker/exchanges/binance_test.rb +83 -0
- data/test/honeymaker/exchanges/binance_us_test.rb +53 -0
- data/test/honeymaker/exchanges/bingx_test.rb +65 -0
- data/test/honeymaker/exchanges/bitget_test.rb +65 -0
- data/test/honeymaker/exchanges/bitrue_test.rb +65 -0
- data/test/honeymaker/exchanges/bitvavo_test.rb +65 -0
- data/test/honeymaker/exchanges/bybit_test.rb +55 -0
- data/test/honeymaker/exchanges/coinbase_test.rb +106 -0
- data/test/honeymaker/exchanges/gemini_catalogue_test.rb +118 -0
- data/test/honeymaker/exchanges/gemini_test.rb +184 -0
- data/test/honeymaker/exchanges/hyperliquid_test.rb +64 -0
- data/test/honeymaker/exchanges/kraken_test.rb +191 -0
- data/test/honeymaker/exchanges/kucoin_test.rb +55 -0
- data/test/honeymaker/exchanges/mexc_test.rb +97 -0
- data/test/honeymaker/honeymaker_test.rb +27 -0
- data/test/honeymaker/result_test.rb +42 -0
- data/test/honeymaker/utils_test.rb +64 -0
- data/test/native/backend_switch_test.rb +53 -0
- data/test/native/convert_test.rb +137 -0
- data/test/native/gem_isolation_test.rb +17 -0
- data/test/native/loader_test.rb +29 -0
- data/test/native/lockstep_test.rb +23 -0
- data/test/native/signing_smoke_test.rb +28 -0
- data/test/parity/kraken_client_parity_test.rb +398 -0
- data/test/parity/kraken_exchange_parity_test.rb +172 -0
- data/test/release/branch_hardening_test.rb +34 -0
- data/test/release/push_gems_test.rb +79 -0
- data/test/smoke.rb +24 -0
- data/test/support/fixture_helper.rb +13 -0
- data/test/support/kraken_signing_smoke.rb +21 -0
- data/test/support/parity_helper.rb +62 -0
- data/test/support/venue_double.rb +100 -0
- data/test/test_helper.rb +9 -0
- metadata +254 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Honeymaker
|
|
4
|
+
module Clients
|
|
5
|
+
class Gemini < Client
|
|
6
|
+
URL = "https://api.gemini.com"
|
|
7
|
+
RATE_LIMITS = { default: 200, orders: 200 }.freeze
|
|
8
|
+
|
|
9
|
+
def get_symbols
|
|
10
|
+
get_public("/v1/symbols")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get_symbol_details(symbol:)
|
|
14
|
+
get_public("/v1/symbols/details/#{symbol}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def get_ticker(symbol:)
|
|
18
|
+
get_public("/v1/pubticker/#{symbol}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def get_candles(symbol:, time_frame:)
|
|
22
|
+
get_public("/v2/candles/#{symbol}/#{time_frame}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def get_price_feed
|
|
26
|
+
get_public("/v1/pricefeed")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def get_approved_addresses(network:)
|
|
30
|
+
post_signed("/v1/approvedAddresses/account/#{network}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_raw_balances
|
|
34
|
+
post_signed("/v1/balances")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get_balances
|
|
38
|
+
result = get_raw_balances
|
|
39
|
+
return result if result.failure?
|
|
40
|
+
|
|
41
|
+
balances = {}
|
|
42
|
+
Array(result.data).each do |balance|
|
|
43
|
+
symbol = balance["currency"]&.upcase
|
|
44
|
+
next unless symbol
|
|
45
|
+
available = BigDecimal((balance["available"] || "0").to_s)
|
|
46
|
+
amount = BigDecimal((balance["amount"] || "0").to_s)
|
|
47
|
+
locked = amount - available
|
|
48
|
+
next if available.zero? && locked.zero?
|
|
49
|
+
balances[symbol] = { free: available, locked: locked }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
Result::Success.new(balances)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def new_order(symbol:, amount:, price:, side:, type:, client_order_id: nil, options: [])
|
|
56
|
+
result = post_signed("/v1/order/new", {
|
|
57
|
+
symbol: symbol, amount: amount, price: price,
|
|
58
|
+
side: side, type: type, client_order_id: client_order_id,
|
|
59
|
+
options: options
|
|
60
|
+
})
|
|
61
|
+
return result if result.failure?
|
|
62
|
+
|
|
63
|
+
raw = result.data
|
|
64
|
+
Result::Success.new({ order_id: raw["order_id"].to_s, raw: raw })
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def order_status(order_id:)
|
|
68
|
+
result = post_signed("/v1/order/status", { order_id: order_id })
|
|
69
|
+
return result if result.failure?
|
|
70
|
+
|
|
71
|
+
raw = result.data
|
|
72
|
+
Result::Success.new(normalize_order(raw["order_id"].to_s, raw))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def cancel_order(order_id:)
|
|
76
|
+
post_signed("/v1/order/cancel", { order_id: order_id })
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def get_my_trades(symbol: nil, limit_trades: nil, timestamp: nil)
|
|
80
|
+
post_signed("/v1/mytrades", { symbol: symbol, limit_trades: limit_trades, timestamp: timestamp })
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def get_transfers(timestamp: nil, limit_transfers: nil)
|
|
84
|
+
post_signed("/v1/transfers", { timestamp: timestamp, limit_transfers: limit_transfers })
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def withdraw(currency:, address:, amount:)
|
|
88
|
+
post_signed("/v1/withdraw/#{currency}", { address: address, amount: amount })
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# --- Staking ---
|
|
92
|
+
|
|
93
|
+
def staking_history
|
|
94
|
+
post_signed("/v1/staking/history")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def staking_rewards
|
|
98
|
+
post_signed("/v1/staking/rewards")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def staking_balances
|
|
102
|
+
post_signed("/v1/balances/staking")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def normalize_order(order_id, raw)
|
|
108
|
+
order_type = parse_order_type(raw["type"])
|
|
109
|
+
side = raw["side"]&.downcase&.to_sym
|
|
110
|
+
status = parse_gemini_order_status(raw)
|
|
111
|
+
|
|
112
|
+
price = BigDecimal((raw["avg_execution_price"] || "0").to_s)
|
|
113
|
+
price = BigDecimal((raw["price"] || "0").to_s) if price.zero?
|
|
114
|
+
price = nil if price.zero?
|
|
115
|
+
|
|
116
|
+
amount = BigDecimal((raw["original_amount"] || "0").to_s)
|
|
117
|
+
amount_exec = BigDecimal((raw["executed_amount"] || "0").to_s)
|
|
118
|
+
quote_amount_exec = price ? (amount_exec * price) : BigDecimal("0")
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
order_id: order_id, status: status, side: side, order_type: order_type,
|
|
122
|
+
price: price, amount: amount, quote_amount: nil,
|
|
123
|
+
amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def parse_order_type(type)
|
|
128
|
+
case type&.downcase
|
|
129
|
+
when "exchange limit", "limit" then :limit
|
|
130
|
+
when "market", "exchange market" then :market
|
|
131
|
+
else :limit
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def parse_gemini_order_status(raw)
|
|
136
|
+
if raw["is_cancelled"]
|
|
137
|
+
:cancelled
|
|
138
|
+
elsif raw["is_live"] && BigDecimal((raw["remaining_amount"] || "0").to_s).positive?
|
|
139
|
+
:open
|
|
140
|
+
elsif !raw["is_live"] && BigDecimal((raw["executed_amount"] || "0").to_s).positive?
|
|
141
|
+
:closed
|
|
142
|
+
else
|
|
143
|
+
:unknown
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def validate_trading_credentials
|
|
148
|
+
result = get_raw_balances
|
|
149
|
+
return Result::Failure.new("Invalid trading credentials") if result.failure?
|
|
150
|
+
result.data.is_a?(Array) ? Result::Success.new(true) : Result::Failure.new("Invalid trading credentials")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def validate_read_credentials
|
|
154
|
+
validate_trading_credentials
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def get_public(path, params = {})
|
|
158
|
+
with_rescue do
|
|
159
|
+
response = connection.get do |req|
|
|
160
|
+
req.url path
|
|
161
|
+
req.params = params.compact
|
|
162
|
+
end
|
|
163
|
+
response.body
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def post_signed(path, body = {})
|
|
168
|
+
with_rescue do
|
|
169
|
+
payload = body.compact.merge(request: path, nonce: timestamp_ms.to_s)
|
|
170
|
+
encoded_payload = Base64.strict_encode64(payload.to_json)
|
|
171
|
+
# HMAC-SHA384, not SHA256: https://developer.gemini.com/authentication/api-key
|
|
172
|
+
signature = OpenSSL::HMAC.hexdigest("sha384", @api_secret, encoded_payload)
|
|
173
|
+
|
|
174
|
+
response = connection.post do |req|
|
|
175
|
+
req.url path
|
|
176
|
+
req.headers = {
|
|
177
|
+
"X-GEMINI-APIKEY": @api_key,
|
|
178
|
+
"X-GEMINI-PAYLOAD": encoded_payload,
|
|
179
|
+
"X-GEMINI-SIGNATURE": signature,
|
|
180
|
+
Accept: "application/json",
|
|
181
|
+
"Content-Type": "text/plain"
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
response.body
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Honeymaker
|
|
4
|
+
module Clients
|
|
5
|
+
class Hyperliquid < Client
|
|
6
|
+
URL = "https://api.hyperliquid.xyz"
|
|
7
|
+
RATE_LIMITS = { default: 200, orders: 200 }.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(api_key: nil, api_secret: nil, proxy: nil, logger: nil)
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def spot_meta
|
|
14
|
+
post_info({ type: "spotMeta" })
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def spot_meta_and_asset_ctxs
|
|
18
|
+
post_info({ type: "spotMetaAndAssetCtxs" })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def spot_clearinghouse_state(user:)
|
|
22
|
+
post_info({ type: "spotClearinghouseState", user: user })
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def all_mids
|
|
26
|
+
post_info({ type: "allMids" })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def spot_balances(user: nil)
|
|
30
|
+
user ||= @api_key
|
|
31
|
+
spot_clearinghouse_state(user: user)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def l2_book(coin:)
|
|
35
|
+
post_info({ type: "l2Book", coin: coin })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def candles_snapshot(coin:, interval:, start_time:, end_time:)
|
|
39
|
+
post_info({ type: "candleSnapshot", req: { coin: coin, interval: interval, startTime: start_time, endTime: end_time } })
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def get_balances(user: nil)
|
|
43
|
+
user ||= @api_key
|
|
44
|
+
result = spot_clearinghouse_state(user: user)
|
|
45
|
+
return result if result.failure?
|
|
46
|
+
|
|
47
|
+
balances = {}
|
|
48
|
+
(result.data["balances"] || []).each do |balance|
|
|
49
|
+
symbol = balance["coin"]
|
|
50
|
+
total = BigDecimal((balance["total"] || "0").to_s)
|
|
51
|
+
hold = BigDecimal((balance["hold"] || "0").to_s)
|
|
52
|
+
free = total - hold
|
|
53
|
+
next if free.zero? && hold.zero?
|
|
54
|
+
balances[symbol] = { free: free, locked: hold }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Result::Success.new(balances)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Hyperliquid's orderStatus body is NESTED:
|
|
61
|
+
# { "status" => "order"|"unknownOid",
|
|
62
|
+
# "order" => { "order" => { coin, side, limitPx, sz(remaining), origSz, oid, timestamp, ... },
|
|
63
|
+
# "status" => <real order status>, "statusTimestamp" => ... } }
|
|
64
|
+
# The real status/sizes live under order["order"]/order["status"] — NOT the top level — and the
|
|
65
|
+
# body carries NO fills. So the ordered amount is origSz, executed is origSz - remaining sz, and the
|
|
66
|
+
# exact cost comes from a bounded userFillsByTime (only fetched when something actually executed,
|
|
67
|
+
# since userFillsByTime is API weight 20 vs orderStatus's weight 2).
|
|
68
|
+
def order_status(user:, oid:)
|
|
69
|
+
result = post_info({ type: "orderStatus", user: user, oid: oid })
|
|
70
|
+
return result if result.failure?
|
|
71
|
+
|
|
72
|
+
raw = result.data
|
|
73
|
+
# A distinct not-found signal — aged-out orders are normal; the caller recovers fills / abandons.
|
|
74
|
+
return Result::Failure.new("unknownOid", data: { not_found: true }) if raw["status"] == "unknownOid"
|
|
75
|
+
|
|
76
|
+
wrapper = raw["order"] || {}
|
|
77
|
+
order = wrapper["order"] || {}
|
|
78
|
+
status_str = wrapper["status"]
|
|
79
|
+
|
|
80
|
+
coin = order["coin"]
|
|
81
|
+
side = order["side"] == "B" ? :buy : :sell
|
|
82
|
+
limit_price = BigDecimal((order["limitPx"] || "0").to_s)
|
|
83
|
+
ordered_size = BigDecimal((order["origSz"] || "0").to_s)
|
|
84
|
+
remaining_size = BigDecimal((order["sz"] || "0").to_s)
|
|
85
|
+
amount_exec = [ordered_size - remaining_size, BigDecimal("0")].max
|
|
86
|
+
|
|
87
|
+
quote_amount_exec = BigDecimal("0")
|
|
88
|
+
price = limit_price
|
|
89
|
+
if amount_exec.positive?
|
|
90
|
+
fills_result = order["timestamp"] ? user_fills_by_time(user: user, start_time: order["timestamp"]) : nil
|
|
91
|
+
# A FAILED exact-cost lookup (timeout / rate-limit) is PROPAGATED so the consumer's typed-error
|
|
92
|
+
# retry runs — never record an executed order with an estimated cost just because userFills
|
|
93
|
+
# blipped (that would silently corrupt accounting and skip the retry).
|
|
94
|
+
return fills_result if fills_result&.failure?
|
|
95
|
+
|
|
96
|
+
matched = Array(fills_result&.data).select { |f| f["oid"].to_s == oid.to_s }
|
|
97
|
+
matched_quote = matched.sum(BigDecimal("0")) { |f| BigDecimal(f["px"].to_s) * BigDecimal(f["sz"].to_s) }
|
|
98
|
+
# userFills SUCCEEDED but has no matching fill (aged out of the window) → estimate from the
|
|
99
|
+
# limit price so a filled order never reports quote_amount_exec 0. Only on success, never failure.
|
|
100
|
+
quote_amount_exec = matched_quote.positive? ? matched_quote : (limit_price * amount_exec)
|
|
101
|
+
price = quote_amount_exec / amount_exec
|
|
102
|
+
end
|
|
103
|
+
price = nil if price.nil? || price.zero?
|
|
104
|
+
|
|
105
|
+
Result::Success.new({
|
|
106
|
+
order_id: "#{coin}-#{oid}", coin: coin,
|
|
107
|
+
status: parse_order_status(status_str), side: side, order_type: :limit,
|
|
108
|
+
price: price, amount: ordered_size, quote_amount: nil,
|
|
109
|
+
amount_exec: amount_exec, quote_amount_exec: quote_amount_exec, raw: raw
|
|
110
|
+
})
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def open_orders(user:)
|
|
114
|
+
post_info({ type: "openOrders", user: user })
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def user_fills(user:, start_time: nil, end_time: nil)
|
|
118
|
+
body = { type: "userFills", user: user }
|
|
119
|
+
body[:startTime] = start_time if start_time
|
|
120
|
+
body[:endTime] = end_time if end_time
|
|
121
|
+
post_info(body)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def user_fills_by_time(user:, start_time:, end_time: nil)
|
|
125
|
+
body = { type: "userFillsByTime", user: user, startTime: start_time }
|
|
126
|
+
body[:endTime] = end_time if end_time
|
|
127
|
+
post_info(body)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# --- Trading (requires hyperliquid-rb gem) ---
|
|
131
|
+
|
|
132
|
+
def order(coin:, is_buy:, size:, limit_px:, order_type: { limit: { tif: "Gtc" } })
|
|
133
|
+
with_status_rescue do
|
|
134
|
+
exchange_client.order(coin, is_buy: is_buy, sz: size, limit_px: limit_px, order_type: order_type)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def cancel(coin:, oid:)
|
|
139
|
+
with_status_rescue do
|
|
140
|
+
exchange_client.cancel(coin, oid)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# --- Futures ---
|
|
145
|
+
|
|
146
|
+
def user_funding(user:, start_time:, end_time: nil)
|
|
147
|
+
body = { type: "userFunding", user: user, startTime: start_time }
|
|
148
|
+
body[:endTime] = end_time if end_time
|
|
149
|
+
post_info(body)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def user_non_funding_ledger_updates(user:, start_time:, end_time: nil)
|
|
153
|
+
body = { type: "userNonFundingLedgerUpdates", user: user, startTime: start_time }
|
|
154
|
+
body[:endTime] = end_time if end_time
|
|
155
|
+
post_info(body)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
# Trading does not go over this client's Faraday connection: it goes through hyperliquid-rb,
|
|
161
|
+
# which raises its own error for every non-2xx instead of answering. Client#with_rescue can
|
|
162
|
+
# only see an unlabelled StandardError there, so it flags it `client_error` — "no clean
|
|
163
|
+
# answer" — and a caller reading an order placement has to treat that as a request whose fate
|
|
164
|
+
# is unknown: nothing recorded as rejected, no retry, a line for a human to reconcile against
|
|
165
|
+
# the venue. But the status is right there on the exception, and it is exactly what tells a
|
|
166
|
+
# refusal (4xx — the gateway answered, nothing reached the matching engine) from a request the
|
|
167
|
+
# venue may still have taken (5xx). Hand it back in the same shape every other HTTP failure
|
|
168
|
+
# has, so one classifier covers both paths.
|
|
169
|
+
#
|
|
170
|
+
# Duck-typed on purpose: hyperliquid-rb is an optional dependency loaded only when trading is
|
|
171
|
+
# used, so its error classes cannot be named here. Anything without a status is classified
|
|
172
|
+
# exactly as it is everywhere else.
|
|
173
|
+
def with_status_rescue
|
|
174
|
+
Result::Success.new(yield)
|
|
175
|
+
rescue StandardError => e
|
|
176
|
+
status = e.respond_to?(:status) ? e.status.to_i : 0
|
|
177
|
+
return with_rescue { raise e } unless status.positive?
|
|
178
|
+
|
|
179
|
+
Result::Failure.new(e.message.to_s.empty? ? e.class.to_s : e.message, data: { status: status })
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Suffix-aware so the whole Hyperliquid cancel family (marginCanceled, scheduledCancel,
|
|
183
|
+
# reduceOnlyCanceled, siblingFilledCanceled, …) maps correctly. A triggered order has fired
|
|
184
|
+
# and become a live resting order → :open. An unmapped status is logged, never swallowed.
|
|
185
|
+
def parse_order_status(status)
|
|
186
|
+
case status
|
|
187
|
+
when "filled" then :closed
|
|
188
|
+
when "open", "triggered" then :open
|
|
189
|
+
else
|
|
190
|
+
str = status.to_s
|
|
191
|
+
if str.match?(/cancel/i) || str.match?(/reject/i)
|
|
192
|
+
:cancelled
|
|
193
|
+
else
|
|
194
|
+
@logger&.warn("[honeymaker] Unmapped Hyperliquid order status: #{status.inspect}")
|
|
195
|
+
:unknown
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def validate_trading_credentials
|
|
201
|
+
return Result::Failure.new("No wallet address provided") unless @api_key
|
|
202
|
+
result = open_orders(user: @api_key)
|
|
203
|
+
result.success? ? Result::Success.new(true) : Result::Failure.new("Invalid credentials")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def validate_read_credentials
|
|
207
|
+
validate_trading_credentials
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def exchange_client
|
|
211
|
+
raise Error, "Trading requires api_secret (agent key)" unless @api_secret && !@api_secret.empty?
|
|
212
|
+
@exchange ||= begin
|
|
213
|
+
require "hyperliquid"
|
|
214
|
+
::Hyperliquid::Exchange.new(private_key: @api_secret)
|
|
215
|
+
rescue LoadError
|
|
216
|
+
raise Error, "Add 'hyperliquid-rb' to your Gemfile to use Hyperliquid trading"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def post_info(body)
|
|
221
|
+
with_rescue do
|
|
222
|
+
response = connection.post do |req|
|
|
223
|
+
req.url "/info"
|
|
224
|
+
req.headers = { Accept: "application/json", "Content-Type": "application/json" }
|
|
225
|
+
req.body = body.to_json
|
|
226
|
+
end
|
|
227
|
+
response.body
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|