hyperliquid 0.4.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 184e215d2d80056cb19bd6a20b273134d9997979d160d154318c5e155cdf02bb
4
- data.tar.gz: 1f2bd8b08ef2e99e3732bd7775eeb70fe00ad7c2f4210312d07d237d28e670ed
3
+ metadata.gz: e4e2d29336f0e921d82f0f7a154676a102a228fd5aa4811b85db890802330865
4
+ data.tar.gz: ff28b26c30bd14549def4e1f5adb3193ab812cc4527da50109dd17d5bc333816
5
5
  SHA512:
6
- metadata.gz: 1b8645b6f40ae9163343fbc6d7d5c814417643469d75ac320073546243d601055c26f92e1ecb272baf9d839dc4b983b19d56be01cc2bcf4709de9f0e6bf81116
7
- data.tar.gz: 462b64553e8ee0939bf0284b0647ec602f9be2f3b2389144e64188813cf52d1bca1daf3feb233eab1167fb1337bda7028208acc38398e55a6279e2593ab26181
6
+ metadata.gz: d19f8ddf0b9d7cb16a7791a2d7117725929e956c05a46a27b11cc9b27bb051f0c4d7b1cfc1053e3683a9577ceaacbd96ebd3b06a3541d527f9c0437e5de067b1
7
+ data.tar.gz: 8a590a96b1e14b8178e3d628cb8e498d718e0a41963dad0671ead93c06df3d389e2fe2c3082d2a27965eb0adf46370e1508f8a9b0cb2e002444c225e39d8aa79
data/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  ## [Ruby Hyperliquid SDK Changelog]
2
2
 
3
- ## [0.4.1] - 2025-01-28
3
+ ## [0.5.0] - 2026-01-28
4
+
5
+ - Add core trading features to Exchange API (Tier 1 parity with Python SDK)
6
+ - Order modification: `modify_order`, `batch_modify`
7
+ - Position management: `update_leverage`, `update_isolated_margin`, `market_close`
8
+ - Dead man's switch: `schedule_cancel`
9
+ - Add `market_close` helper for closing positions at market price with auto-detection of size and direction
10
+ - Add integration tests for leverage updates, order modification, and market close
11
+ - Add GitHub Release workflow (`.github/workflows/release.yml`)
12
+
13
+ ## [0.4.1] - 2026-01-28
4
14
 
5
15
  - Reorganize documentation for improved readability
6
16
  - Streamline README with basic setup and links to detailed docs
@@ -10,7 +20,7 @@
10
20
  - Add `docs/ERRORS.md` with error handling guide
11
21
  - Add `docs/DEVELOPMENT.md` with setup and testing instructions
12
22
 
13
- ## [0.4.0] - 2025-01-27
23
+ ## [0.4.0] - 2026-01-27
14
24
 
15
25
  - Add Exchange API for authenticated write operations (trading)
16
26
  - Order placement: `order`, `bulk_orders`, `market_order`
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Hyperliquid Ruby SDK
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/hyperliquid.svg)](https://rubygems.org/gems/hyperliquid)
4
+ [![Downloads](https://img.shields.io/gem/dt/hyperliquid.svg)](https://rubygems.org/gems/hyperliquid)
5
+ [![CI](https://github.com/carter2099/hyperliquid/actions/workflows/main.yml/badge.svg)](https://github.com/carter2099/hyperliquid/actions)
6
+
3
7
  A Ruby SDK for interacting with the Hyperliquid decentralized exchange API.
4
8
 
5
9
  The SDK supports both read operations (Info API) and authenticated write operations (Exchange API) for trading.
@@ -54,14 +58,6 @@ exchange = trading_sdk.exchange
54
58
  - [Error Handling](docs/ERRORS.md) - Error types and handling
55
59
  - [Development](docs/DEVELOPMENT.md) - Contributing and running tests
56
60
 
57
- ## Roadmap
58
-
59
- The SDK now supports both Info API (read) and Exchange API (trading). Future versions will include:
60
-
61
- - WebSocket support for real-time data
62
- - Additional exchange operations (leverage, margin adjustments, transfers)
63
- - Advanced trading features (TWAP, etc.)
64
-
65
61
  ## Contributing
66
62
 
67
63
  Bug reports and pull requests are welcome on GitHub at https://github.com/carter2099/hyperliquid.
data/SECURITY.md ADDED
@@ -0,0 +1,7 @@
1
+ # Security Policy
2
+
3
+ ## For Issues in Hyperliquid
4
+
5
+ If you have discovered a vulnerability or security issue related to the Hyperliquid service (e.g., buffer overflow, SQL
6
+ injection, cross-site scripting, etc.), please refer to the
7
+ [Hyperliquid Security Policy](https://github.com/hyperliquid-dex/hyperliquid-python-sdk/blob/master/SECURITY.md).
data/docs/API.md CHANGED
@@ -59,13 +59,33 @@ Read-only methods for querying market data and user information.
59
59
 
60
60
  **Note:** Exchange methods require initializing the SDK with a `private_key`.
61
61
 
62
+ ### Order Placement
63
+
62
64
  - `order(coin:, is_buy:, size:, limit_px:, ...)` - Place a single limit order
63
65
  - `bulk_orders(orders:, grouping:, ...)` - Place multiple orders in a batch
64
66
  - `market_order(coin:, is_buy:, size:, slippage:, ...)` - Place a market order with slippage
67
+
68
+ ### Order Modification
69
+
70
+ - `modify_order(oid:, coin:, is_buy:, size:, limit_px:, ...)` - Modify an existing order by oid or cloid
71
+ - `batch_modify(modifies:, ...)` - Modify multiple orders at once
72
+
73
+ ### Order Cancellation
74
+
65
75
  - `cancel(coin:, oid:, ...)` - Cancel an order by order ID
66
76
  - `cancel_by_cloid(coin:, cloid:, ...)` - Cancel an order by client order ID
67
77
  - `bulk_cancel(cancels:, ...)` - Cancel multiple orders by order ID
68
78
  - `bulk_cancel_by_cloid(cancels:, ...)` - Cancel multiple orders by client order ID
79
+ - `schedule_cancel(time:, ...)` - Auto-cancel all orders at a given time
80
+
81
+ ### Position Management
82
+
83
+ - `market_close(coin:, size:, slippage:, ...)` - Close a position at market price (auto-detects position size)
84
+ - `update_leverage(coin:, leverage:, is_cross:, ...)` - Set cross or isolated leverage for a coin
85
+ - `update_isolated_margin(coin:, amount:, ...)` - Add or remove isolated margin for a position
86
+
87
+ ### Other
88
+
69
89
  - `address` - Get the wallet address associated with the private key
70
90
 
71
91
  All exchange methods support an optional `vault_address:` parameter for vault trading.
data/docs/EXAMPLES.md CHANGED
@@ -276,6 +276,70 @@ cloid_cancels = [
276
276
  sdk.exchange.bulk_cancel_by_cloid(cancels: cloid_cancels)
277
277
  ```
278
278
 
279
+ ### Modifying Orders
280
+
281
+ ```ruby
282
+ # Modify an existing order by order ID
283
+ oid = result.dig('response', 'data', 'statuses', 0, 'resting', 'oid')
284
+ sdk.exchange.modify_order(
285
+ oid: oid,
286
+ coin: 'BTC',
287
+ is_buy: true,
288
+ size: '0.02',
289
+ limit_px: '96000'
290
+ )
291
+
292
+ # Modify an order by client order ID
293
+ cloid = Hyperliquid::Cloid.from_int(123)
294
+ sdk.exchange.modify_order(
295
+ oid: cloid,
296
+ coin: 'BTC',
297
+ is_buy: true,
298
+ size: '0.02',
299
+ limit_px: '96000'
300
+ )
301
+
302
+ # Modify multiple orders at once
303
+ modifies = [
304
+ { oid: 12345, coin: 'BTC', is_buy: true, size: '0.01', limit_px: '95000' },
305
+ { oid: 12346, coin: 'ETH', is_buy: false, size: '0.5', limit_px: '3200' }
306
+ ]
307
+ sdk.exchange.batch_modify(modifies: modifies)
308
+ ```
309
+
310
+ ### Position Management
311
+
312
+ ```ruby
313
+ # Set cross leverage (default)
314
+ sdk.exchange.update_leverage(coin: 'BTC', leverage: 5)
315
+
316
+ # Set isolated leverage
317
+ sdk.exchange.update_leverage(coin: 'BTC', leverage: 10, is_cross: false)
318
+
319
+ # Add isolated margin to a position (positive amount)
320
+ sdk.exchange.update_isolated_margin(coin: 'BTC', amount: 100.0)
321
+
322
+ # Remove isolated margin from a position (negative amount)
323
+ sdk.exchange.update_isolated_margin(coin: 'BTC', amount: -50.0)
324
+
325
+ # Close an entire position at market price (auto-detects size and direction)
326
+ sdk.exchange.market_close(coin: 'BTC')
327
+
328
+ # Close a partial position with custom slippage
329
+ sdk.exchange.market_close(coin: 'BTC', size: 0.01, slippage: 0.03)
330
+ ```
331
+
332
+ ### Schedule Cancel
333
+
334
+ ```ruby
335
+ # Schedule auto-cancel of all orders at a specific time
336
+ cancel_time = (Time.now.to_f * 1000).to_i + 60_000 # 60 seconds from now
337
+ sdk.exchange.schedule_cancel(time: cancel_time)
338
+
339
+ # Activate schedule cancel without specifying a time (server default)
340
+ sdk.exchange.schedule_cancel
341
+ ```
342
+
279
343
  ### Vault Trading
280
344
 
281
345
  ```ruby
@@ -213,6 +213,167 @@ module Hyperliquid
213
213
  post_action(action, signature, nonce, vault_address)
214
214
  end
215
215
 
216
+ # Modify a single existing order
217
+ # @param oid [Integer, Cloid, String] Order ID or client order ID to modify
218
+ # @param coin [String] Asset symbol (e.g., "BTC")
219
+ # @param is_buy [Boolean] True for buy, false for sell
220
+ # @param size [String, Numeric] New order size
221
+ # @param limit_px [String, Numeric] New limit price
222
+ # @param order_type [Hash] Order type config (default: { limit: { tif: "Gtc" } })
223
+ # @param reduce_only [Boolean] Reduce-only flag (default: false)
224
+ # @param cloid [Cloid, String, nil] Client order ID for the modified order (optional)
225
+ # @param vault_address [String, nil] Vault address for vault trading (optional)
226
+ # @return [Hash] Modify response
227
+ def modify_order(oid:, coin:, is_buy:, size:, limit_px:,
228
+ order_type: { limit: { tif: 'Gtc' } },
229
+ reduce_only: false, cloid: nil, vault_address: nil)
230
+ batch_modify(
231
+ modifies: [{
232
+ oid: oid, coin: coin, is_buy: is_buy, size: size,
233
+ limit_px: limit_px, order_type: order_type,
234
+ reduce_only: reduce_only, cloid: cloid
235
+ }],
236
+ vault_address: vault_address
237
+ )
238
+ end
239
+
240
+ # Modify multiple orders at once
241
+ # @param modifies [Array<Hash>] Array of modify hashes with keys:
242
+ # :oid, :coin, :is_buy, :size, :limit_px, :order_type, :reduce_only, :cloid
243
+ # @param vault_address [String, nil] Vault address for vault trading (optional)
244
+ # @return [Hash] Batch modify response
245
+ def batch_modify(modifies:, vault_address: nil)
246
+ nonce = timestamp_ms
247
+
248
+ modify_wires = modifies.map do |m|
249
+ order_wire = build_order_wire(
250
+ coin: m[:coin],
251
+ is_buy: m[:is_buy],
252
+ size: m[:size],
253
+ limit_px: m[:limit_px],
254
+ order_type: m[:order_type] || { limit: { tif: 'Gtc' } },
255
+ reduce_only: m[:reduce_only] || false,
256
+ cloid: m[:cloid]
257
+ )
258
+ { oid: normalize_oid(m[:oid]), order: order_wire }
259
+ end
260
+
261
+ action = {
262
+ type: 'batchModify',
263
+ modifies: modify_wires
264
+ }
265
+
266
+ signature = @signer.sign_l1_action(
267
+ action, nonce,
268
+ vault_address: vault_address,
269
+ expires_after: @expires_after
270
+ )
271
+ post_action(action, signature, nonce, vault_address)
272
+ end
273
+
274
+ # Set cross or isolated leverage for a coin
275
+ # @param coin [String] Asset symbol (perps only)
276
+ # @param leverage [Integer] Leverage value
277
+ # @param is_cross [Boolean] True for cross margin, false for isolated (default: true)
278
+ # @param vault_address [String, nil] Vault address for vault trading (optional)
279
+ # @return [Hash] Leverage update response
280
+ def update_leverage(coin:, leverage:, is_cross: true, vault_address: nil)
281
+ nonce = timestamp_ms
282
+
283
+ action = {
284
+ type: 'updateLeverage',
285
+ asset: asset_index(coin),
286
+ isCross: is_cross,
287
+ leverage: leverage
288
+ }
289
+
290
+ signature = @signer.sign_l1_action(
291
+ action, nonce,
292
+ vault_address: vault_address,
293
+ expires_after: @expires_after
294
+ )
295
+ post_action(action, signature, nonce, vault_address)
296
+ end
297
+
298
+ # Add or remove isolated margin for a position
299
+ # @param coin [String] Asset symbol (perps only)
300
+ # @param amount [Numeric] Amount in USD (positive to add, negative to remove)
301
+ # @param vault_address [String, nil] Vault address for vault trading (optional)
302
+ # @return [Hash] Margin update response
303
+ def update_isolated_margin(coin:, amount:, vault_address: nil)
304
+ nonce = timestamp_ms
305
+
306
+ action = {
307
+ type: 'updateIsolatedMargin',
308
+ asset: asset_index(coin),
309
+ isBuy: true,
310
+ ntli: float_to_usd_int(amount)
311
+ }
312
+
313
+ signature = @signer.sign_l1_action(
314
+ action, nonce,
315
+ vault_address: vault_address,
316
+ expires_after: @expires_after
317
+ )
318
+ post_action(action, signature, nonce, vault_address)
319
+ end
320
+
321
+ # Schedule automatic cancellation of all orders
322
+ # @param time [Integer, nil] UTC timestamp in milliseconds to cancel at (nil to activate with server default)
323
+ # @param vault_address [String, nil] Vault address for vault trading (optional)
324
+ # @return [Hash] Schedule cancel response
325
+ def schedule_cancel(time: nil, vault_address: nil)
326
+ nonce = timestamp_ms
327
+
328
+ action = { type: 'scheduleCancel' }
329
+ action[:time] = time if time
330
+
331
+ signature = @signer.sign_l1_action(
332
+ action, nonce,
333
+ vault_address: vault_address,
334
+ expires_after: @expires_after
335
+ )
336
+ post_action(action, signature, nonce, vault_address)
337
+ end
338
+
339
+ # Close a position at market price
340
+ # @param coin [String] Asset symbol (perps only)
341
+ # @param size [Numeric, nil] Size to close (nil = close entire position)
342
+ # @param slippage [Float] Slippage tolerance (default: 5%)
343
+ # @param cloid [Cloid, String, nil] Client order ID (optional)
344
+ # @param vault_address [String, nil] Vault address for vault trading (optional)
345
+ # @return [Hash] Order response
346
+ def market_close(coin:, size: nil, slippage: DEFAULT_SLIPPAGE, cloid: nil, vault_address: nil)
347
+ address = vault_address || @signer.address
348
+ state = @info.user_state(address)
349
+
350
+ position = state['assetPositions']&.find do |pos|
351
+ pos.dig('position', 'coin') == coin
352
+ end
353
+ raise ArgumentError, "No open position found for #{coin}" unless position
354
+
355
+ szi = position.dig('position', 'szi').to_f
356
+ is_buy = szi.negative?
357
+ close_size = size || szi.abs
358
+
359
+ mids = @info.all_mids
360
+ mid = mids[coin]&.to_f
361
+ raise ArgumentError, "Unknown asset or no price available: #{coin}" unless mid&.positive?
362
+
363
+ slippage_price = calculate_slippage_price(coin, mid, is_buy, slippage)
364
+
365
+ order(
366
+ coin: coin,
367
+ is_buy: is_buy,
368
+ size: close_size,
369
+ limit_px: slippage_price,
370
+ order_type: { limit: { tif: 'Ioc' } },
371
+ reduce_only: true,
372
+ cloid: cloid,
373
+ vault_address: vault_address
374
+ )
375
+ end
376
+
216
377
  # Clear the asset metadata cache
217
378
  # Call this if metadata has been updated
218
379
  def reload_metadata!
@@ -345,6 +506,29 @@ module Hyperliquid
345
506
  format("%.#{decimal_places}f", rounded)
346
507
  end
347
508
 
509
+ # Normalize an order ID to the correct wire format
510
+ # @param oid [Integer, Cloid, String] Order ID or client order ID
511
+ # @return [Integer, String] Normalized order ID
512
+ def normalize_oid(oid)
513
+ case oid
514
+ when Integer then oid
515
+ when Cloid then oid.to_raw
516
+ when String then normalize_cloid(oid)
517
+ else raise ArgumentError, "oid must be Integer, Cloid, or String. Got: #{oid.class}"
518
+ end
519
+ end
520
+
521
+ # Convert a float USD amount to an integer (scaled by 10^6)
522
+ # @param value [Numeric] USD amount
523
+ # @return [Integer] Scaled integer value
524
+ def float_to_usd_int(value)
525
+ scaled = value.to_f * 1_000_000
526
+ rounded = scaled.round
527
+ raise ArgumentError, "float_to_usd_int causes rounding: #{value}" if (rounded - scaled).abs >= 1e-3
528
+
529
+ rounded
530
+ end
531
+
348
532
  # Convert cloid to raw string format
349
533
  # @param cloid [Cloid, String, nil] Client order ID
350
534
  # @return [String, nil] Raw cloid string
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hyperliquid
4
- VERSION = '0.4.1'
4
+ VERSION = '0.5.0'
5
5
  end
data/test_integration.rb CHANGED
@@ -246,6 +246,127 @@ else
246
246
  puts "SKIPPED: Could not get #{perp_coin} price"
247
247
  end
248
248
 
249
+ # ============================================================
250
+ # TEST 5: Update Leverage (BTC)
251
+ # ============================================================
252
+ separator('TEST 5: Update Leverage (BTC)')
253
+
254
+ if btc_price&.positive?
255
+ puts 'Setting BTC to 5x cross leverage...'
256
+ result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 5, is_cross: true)
257
+ puts "Result: #{result.inspect}"
258
+ puts
259
+
260
+ wait_with_countdown(WAIT_SECONDS, 'Waiting before next leverage update...')
261
+
262
+ puts 'Setting BTC to 3x isolated leverage...'
263
+ result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 3, is_cross: false)
264
+ puts "Result: #{result.inspect}"
265
+ puts
266
+
267
+ wait_with_countdown(WAIT_SECONDS, 'Waiting before resetting leverage...')
268
+
269
+ puts 'Resetting BTC to 1x cross leverage...'
270
+ result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 1, is_cross: true)
271
+ puts "Result: #{result.inspect}"
272
+ else
273
+ puts "SKIPPED: Could not get #{perp_coin} price"
274
+ end
275
+
276
+ # ============================================================
277
+ # TEST 6: Modify Order (BTC)
278
+ # ============================================================
279
+ separator('TEST 6: Modify Order (BTC)')
280
+
281
+ if btc_price&.positive?
282
+ # Place limit buy well below market (won't fill)
283
+ original_price = (btc_price * 0.50).round(0).to_i
284
+ modified_price = (btc_price * 0.51).round(0).to_i
285
+ perp_size = (20.0 / btc_price).ceil(sz_decimals)
286
+
287
+ puts "#{perp_coin} mid: $#{btc_price.round(2)}"
288
+ puts "Original limit: $#{original_price} (50% below mid)"
289
+ puts "Modified limit: $#{modified_price} (49% below mid)"
290
+ puts "Size: #{perp_size} BTC"
291
+ puts
292
+
293
+ puts 'Placing limit BUY order...'
294
+ result = sdk.exchange.order(
295
+ coin: perp_coin,
296
+ is_buy: true,
297
+ size: perp_size,
298
+ limit_px: original_price,
299
+ order_type: { limit: { tif: 'Gtc' } }
300
+ )
301
+ oid = check_result(result, 'Limit buy')
302
+
303
+ if oid.is_a?(Integer)
304
+ wait_with_countdown(WAIT_SECONDS, 'Order resting. Waiting before modify...')
305
+
306
+ puts "Modifying order #{oid} (price: $#{original_price} -> $#{modified_price})..."
307
+ result = sdk.exchange.modify_order(
308
+ oid: oid,
309
+ coin: perp_coin,
310
+ is_buy: true,
311
+ size: perp_size,
312
+ limit_px: modified_price
313
+ )
314
+ puts "Modify result: #{result.inspect}"
315
+ puts
316
+
317
+ # Extract new oid from modify result if available
318
+ new_status = result.dig('response', 'data', 'statuses', 0)
319
+ new_oid = if new_status.is_a?(Hash) && new_status['resting']
320
+ new_status['resting']['oid']
321
+ else
322
+ oid
323
+ end
324
+
325
+ wait_with_countdown(WAIT_SECONDS, 'Waiting before cancel...')
326
+
327
+ puts "Canceling modified order #{new_oid}..."
328
+ result = sdk.exchange.cancel(coin: perp_coin, oid: new_oid)
329
+ check_result(result, 'Cancel')
330
+ end
331
+ else
332
+ puts "SKIPPED: Could not get #{perp_coin} price"
333
+ end
334
+
335
+ # ============================================================
336
+ # TEST 7: Market Close (BTC)
337
+ # ============================================================
338
+ separator('TEST 7: Market Close (BTC)')
339
+
340
+ if btc_price&.positive?
341
+ perp_size = (20.0 / btc_price).ceil(sz_decimals)
342
+
343
+ puts "#{perp_coin} mid: $#{btc_price.round(2)}"
344
+ puts "Size: #{perp_size} BTC"
345
+ puts
346
+
347
+ # Open a small long position
348
+ puts 'Opening LONG position (market buy)...'
349
+ result = sdk.exchange.market_order(
350
+ coin: perp_coin,
351
+ is_buy: true,
352
+ size: perp_size,
353
+ slippage: PERP_SLIPPAGE
354
+ )
355
+ check_result(result, 'Long open')
356
+
357
+ wait_with_countdown(WAIT_SECONDS, 'Position open. Waiting before market_close...')
358
+
359
+ # Close using market_close (auto-detect size)
360
+ puts 'Closing position using market_close (auto-detect size)...'
361
+ result = sdk.exchange.market_close(
362
+ coin: perp_coin,
363
+ slippage: PERP_SLIPPAGE
364
+ )
365
+ check_result(result, 'Market close')
366
+ else
367
+ puts "SKIPPED: Could not get #{perp_coin} price"
368
+ end
369
+
249
370
  # ============================================================
250
371
  # Summary
251
372
  # ============================================================
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperliquid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - carter2099
@@ -82,6 +82,7 @@ files:
82
82
  - LICENSE.txt
83
83
  - README.md
84
84
  - Rakefile
85
+ - SECURITY.md
85
86
  - docs/API.md
86
87
  - docs/CONFIGURATION.md
87
88
  - docs/DEVELOPMENT.md