hyperliquid 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.
@@ -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.0'
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.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - carter2099
@@ -82,6 +82,12 @@ files:
82
82
  - LICENSE.txt
83
83
  - README.md
84
84
  - Rakefile
85
+ - SECURITY.md
86
+ - docs/API.md
87
+ - docs/CONFIGURATION.md
88
+ - docs/DEVELOPMENT.md
89
+ - docs/ERRORS.md
90
+ - docs/EXAMPLES.md
85
91
  - example.rb
86
92
  - lib/hyperliquid.rb
87
93
  - lib/hyperliquid/client.rb