hyperliquid 0.4.1 → 0.6.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,362 @@ 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
+
377
+ # Transfer USDC to another address
378
+ # @param amount [String, Numeric] Amount to send
379
+ # @param destination [String] Destination wallet address
380
+ # @return [Hash] Transfer response
381
+ def usd_send(amount:, destination:)
382
+ nonce = timestamp_ms
383
+ action = {
384
+ type: 'usdSend',
385
+ signatureChainId: '0x66eee',
386
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
387
+ destination: destination,
388
+ amount: amount.to_s,
389
+ time: nonce
390
+ }
391
+ signature = @signer.sign_user_signed_action(
392
+ { destination: destination, amount: amount.to_s, time: nonce },
393
+ 'HyperliquidTransaction:UsdSend',
394
+ Signing::EIP712::USD_SEND_TYPES
395
+ )
396
+ post_action(action, signature, nonce, nil)
397
+ end
398
+
399
+ # Transfer a spot token to another address
400
+ # @param amount [String, Numeric] Amount to send
401
+ # @param destination [String] Destination wallet address
402
+ # @param token [String] Token identifier
403
+ # @return [Hash] Transfer response
404
+ def spot_send(amount:, destination:, token:)
405
+ nonce = timestamp_ms
406
+ action = {
407
+ type: 'spotSend',
408
+ signatureChainId: '0x66eee',
409
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
410
+ destination: destination,
411
+ token: token,
412
+ amount: amount.to_s,
413
+ time: nonce
414
+ }
415
+ signature = @signer.sign_user_signed_action(
416
+ { destination: destination, token: token, amount: amount.to_s, time: nonce },
417
+ 'HyperliquidTransaction:SpotSend',
418
+ Signing::EIP712::SPOT_SEND_TYPES
419
+ )
420
+ post_action(action, signature, nonce, nil)
421
+ end
422
+
423
+ # Move USDC between perp and spot accounts
424
+ # @param amount [String, Numeric] Amount to transfer
425
+ # @param to_perp [Boolean] True to move to perp, false to move to spot
426
+ # @return [Hash] Transfer response
427
+ def usd_class_transfer(amount:, to_perp:)
428
+ nonce = timestamp_ms
429
+ action = {
430
+ type: 'usdClassTransfer',
431
+ signatureChainId: '0x66eee',
432
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
433
+ amount: amount.to_s,
434
+ toPerp: to_perp,
435
+ nonce: nonce
436
+ }
437
+ signature = @signer.sign_user_signed_action(
438
+ { amount: amount.to_s, toPerp: to_perp, nonce: nonce },
439
+ 'HyperliquidTransaction:UsdClassTransfer',
440
+ Signing::EIP712::USD_CLASS_TRANSFER_TYPES
441
+ )
442
+ post_action(action, signature, nonce, nil)
443
+ end
444
+
445
+ # Withdraw USDC via the bridge
446
+ # @param amount [String, Numeric] Amount to withdraw
447
+ # @param destination [String] Destination wallet address
448
+ # @return [Hash] Withdrawal response
449
+ def withdraw_from_bridge(amount:, destination:)
450
+ nonce = timestamp_ms
451
+ action = {
452
+ type: 'withdraw3',
453
+ signatureChainId: '0x66eee',
454
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
455
+ destination: destination,
456
+ amount: amount.to_s,
457
+ time: nonce
458
+ }
459
+ signature = @signer.sign_user_signed_action(
460
+ { destination: destination, amount: amount.to_s, time: nonce },
461
+ 'HyperliquidTransaction:Withdraw',
462
+ Signing::EIP712::WITHDRAW_TYPES
463
+ )
464
+ post_action(action, signature, nonce, nil)
465
+ end
466
+
467
+ # Move assets between DEX instances
468
+ # @param destination [String] Destination wallet address
469
+ # @param source_dex [String] Source DEX identifier
470
+ # @param destination_dex [String] Destination DEX identifier
471
+ # @param token [String] Token identifier
472
+ # @param amount [String, Numeric] Amount to send
473
+ # @return [Hash] Transfer response
474
+ def send_asset(destination:, source_dex:, destination_dex:, token:, amount:)
475
+ nonce = timestamp_ms
476
+ action = {
477
+ type: 'sendAsset',
478
+ signatureChainId: '0x66eee',
479
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
480
+ destination: destination,
481
+ sourceDex: source_dex,
482
+ destinationDex: destination_dex,
483
+ token: token,
484
+ amount: amount.to_s,
485
+ fromSubAccount: '',
486
+ nonce: nonce
487
+ }
488
+ signature = @signer.sign_user_signed_action(
489
+ {
490
+ destination: destination, sourceDex: source_dex, destinationDex: destination_dex,
491
+ token: token, amount: amount.to_s, fromSubAccount: '', nonce: nonce
492
+ },
493
+ 'HyperliquidTransaction:SendAsset',
494
+ Signing::EIP712::SEND_ASSET_TYPES
495
+ )
496
+ post_action(action, signature, nonce, nil)
497
+ end
498
+
499
+ # Create a sub-account
500
+ # @param name [String] Sub-account name
501
+ # @return [Hash] Creation response
502
+ def create_sub_account(name:)
503
+ nonce = timestamp_ms
504
+ action = { type: 'createSubAccount', name: name }
505
+ signature = @signer.sign_l1_action(action, nonce)
506
+ post_action(action, signature, nonce, nil)
507
+ end
508
+
509
+ # Transfer USDC to/from a sub-account
510
+ # @param sub_account_user [String] Sub-account wallet address
511
+ # @param is_deposit [Boolean] True to deposit into sub-account, false to withdraw
512
+ # @param usd [Numeric] Amount in USD
513
+ # @return [Hash] Transfer response
514
+ def sub_account_transfer(sub_account_user:, is_deposit:, usd:)
515
+ nonce = timestamp_ms
516
+ action = {
517
+ type: 'subAccountTransfer',
518
+ subAccountUser: sub_account_user,
519
+ isDeposit: is_deposit,
520
+ usd: float_to_usd_int(usd)
521
+ }
522
+ signature = @signer.sign_l1_action(action, nonce)
523
+ post_action(action, signature, nonce, nil)
524
+ end
525
+
526
+ # Transfer spot tokens to/from a sub-account
527
+ # @param sub_account_user [String] Sub-account wallet address
528
+ # @param is_deposit [Boolean] True to deposit into sub-account, false to withdraw
529
+ # @param token [String] Token identifier
530
+ # @param amount [String, Numeric] Amount to transfer
531
+ # @return [Hash] Transfer response
532
+ def sub_account_spot_transfer(sub_account_user:, is_deposit:, token:, amount:)
533
+ nonce = timestamp_ms
534
+ action = {
535
+ type: 'subAccountSpotTransfer',
536
+ subAccountUser: sub_account_user,
537
+ isDeposit: is_deposit,
538
+ token: token,
539
+ amount: amount.to_s
540
+ }
541
+ signature = @signer.sign_l1_action(action, nonce)
542
+ post_action(action, signature, nonce, nil)
543
+ end
544
+
545
+ # Deposit or withdraw USDC to/from a vault
546
+ # @param vault_address [String] Vault wallet address
547
+ # @param is_deposit [Boolean] True to deposit, false to withdraw
548
+ # @param usd [Numeric] Amount in USD
549
+ # @return [Hash] Vault transfer response
550
+ def vault_transfer(vault_address:, is_deposit:, usd:)
551
+ nonce = timestamp_ms
552
+ action = {
553
+ type: 'vaultTransfer',
554
+ vaultAddress: vault_address,
555
+ isDeposit: is_deposit,
556
+ usd: float_to_usd_int(usd)
557
+ }
558
+ signature = @signer.sign_l1_action(action, nonce)
559
+ post_action(action, signature, nonce, nil)
560
+ end
561
+
562
+ # Set referral code
563
+ # @param code [String] Referral code
564
+ # @return [Hash] Set referrer response
565
+ def set_referrer(code:)
566
+ nonce = timestamp_ms
567
+ action = { type: 'setReferrer', code: code }
568
+ signature = @signer.sign_l1_action(action, nonce)
569
+ post_action(action, signature, nonce, nil)
570
+ end
571
+
216
572
  # Clear the asset metadata cache
217
573
  # Call this if metadata has been updated
218
574
  def reload_metadata!
@@ -345,6 +701,29 @@ module Hyperliquid
345
701
  format("%.#{decimal_places}f", rounded)
346
702
  end
347
703
 
704
+ # Normalize an order ID to the correct wire format
705
+ # @param oid [Integer, Cloid, String] Order ID or client order ID
706
+ # @return [Integer, String] Normalized order ID
707
+ def normalize_oid(oid)
708
+ case oid
709
+ when Integer then oid
710
+ when Cloid then oid.to_raw
711
+ when String then normalize_cloid(oid)
712
+ else raise ArgumentError, "oid must be Integer, Cloid, or String. Got: #{oid.class}"
713
+ end
714
+ end
715
+
716
+ # Convert a float USD amount to an integer (scaled by 10^6)
717
+ # @param value [Numeric] USD amount
718
+ # @return [Integer] Scaled integer value
719
+ def float_to_usd_int(value)
720
+ scaled = value.to_f * 1_000_000
721
+ rounded = scaled.round
722
+ raise ArgumentError, "float_to_usd_int causes rounding: #{value}" if (rounded - scaled).abs >= 1e-3
723
+
724
+ rounded
725
+ end
726
+
348
727
  # Convert cloid to raw string format
349
728
  # @param cloid [Cloid, String, nil] Client order ID
350
729
  # @return [String, nil] Raw cloid string
@@ -12,6 +12,61 @@ module Hyperliquid
12
12
  MAINNET_SOURCE = 'a'
13
13
  TESTNET_SOURCE = 'b'
14
14
 
15
+ # Chain ID for user-signed actions (Arbitrum Sepolia: 0x66eee = 421614)
16
+ USER_SIGNED_CHAIN_ID = 421_614
17
+
18
+ # EIP-712 type definitions for user-signed actions
19
+
20
+ USD_SEND_TYPES = {
21
+ 'HyperliquidTransaction:UsdSend': [
22
+ { name: :hyperliquidChain, type: 'string' },
23
+ { name: :destination, type: 'string' },
24
+ { name: :amount, type: 'string' },
25
+ { name: :time, type: 'uint64' }
26
+ ]
27
+ }.freeze
28
+
29
+ SPOT_SEND_TYPES = {
30
+ 'HyperliquidTransaction:SpotSend': [
31
+ { name: :hyperliquidChain, type: 'string' },
32
+ { name: :destination, type: 'string' },
33
+ { name: :token, type: 'string' },
34
+ { name: :amount, type: 'string' },
35
+ { name: :time, type: 'uint64' }
36
+ ]
37
+ }.freeze
38
+
39
+ USD_CLASS_TRANSFER_TYPES = {
40
+ 'HyperliquidTransaction:UsdClassTransfer': [
41
+ { name: :hyperliquidChain, type: 'string' },
42
+ { name: :amount, type: 'string' },
43
+ { name: :toPerp, type: 'bool' },
44
+ { name: :nonce, type: 'uint64' }
45
+ ]
46
+ }.freeze
47
+
48
+ WITHDRAW_TYPES = {
49
+ 'HyperliquidTransaction:Withdraw': [
50
+ { name: :hyperliquidChain, type: 'string' },
51
+ { name: :destination, type: 'string' },
52
+ { name: :amount, type: 'string' },
53
+ { name: :time, type: 'uint64' }
54
+ ]
55
+ }.freeze
56
+
57
+ SEND_ASSET_TYPES = {
58
+ 'HyperliquidTransaction:SendAsset': [
59
+ { name: :hyperliquidChain, type: 'string' },
60
+ { name: :destination, type: 'string' },
61
+ { name: :sourceDex, type: 'string' },
62
+ { name: :destinationDex, type: 'string' },
63
+ { name: :token, type: 'string' },
64
+ { name: :amount, type: 'string' },
65
+ { name: :fromSubAccount, type: 'string' },
66
+ { name: :nonce, type: 'uint64' }
67
+ ]
68
+ }.freeze
69
+
15
70
  class << self
16
71
  # Domain for L1 actions (orders, cancels, leverage, etc.)
17
72
  # @return [Hash] EIP-712 domain configuration
@@ -24,6 +79,17 @@ module Hyperliquid
24
79
  }
25
80
  end
26
81
 
82
+ # Domain for user-signed actions (transfers, withdrawals, etc.)
83
+ # @return [Hash] EIP-712 domain configuration
84
+ def user_signed_domain
85
+ {
86
+ name: 'HyperliquidSignTransaction',
87
+ version: '1',
88
+ chainId: USER_SIGNED_CHAIN_ID,
89
+ verifyingContract: '0x0000000000000000000000000000000000000000'
90
+ }
91
+ end
92
+
27
93
  # EIP-712 domain type definition
28
94
  # @return [Array<Hash>] Domain type fields
29
95
  def domain_type
@@ -50,6 +116,13 @@ module Hyperliquid
50
116
  def source(testnet:)
51
117
  testnet ? TESTNET_SOURCE : MAINNET_SOURCE
52
118
  end
119
+
120
+ # Get hyperliquid chain name for user-signed actions
121
+ # @param testnet [Boolean] Whether testnet
122
+ # @return [String] "Mainnet" or "Testnet"
123
+ def hyperliquid_chain(testnet:)
124
+ testnet ? 'Testnet' : 'Mainnet'
125
+ end
53
126
  end
54
127
  end
55
128
  end
@@ -44,6 +44,31 @@ module Hyperliquid
44
44
  sign_typed_data(typed_data)
45
45
  end
46
46
 
47
+ # Sign a user-signed action (transfers, withdrawals, etc.)
48
+ # Uses direct EIP-712 typed data signing with HyperliquidSignTransaction domain
49
+ # @param action [Hash] The action message to sign (will have chain fields injected)
50
+ # @param primary_type [String] EIP-712 primary type (e.g., "HyperliquidTransaction:UsdSend")
51
+ # @param sign_types [Hash] EIP-712 type definitions for the action
52
+ # @return [Hash] Signature with :r, :s, :v components
53
+ def sign_user_signed_action(action, primary_type, sign_types)
54
+ # Inject chain fields into a copy of the action
55
+ message = action.merge(
56
+ hyperliquidChain: EIP712.hyperliquid_chain(testnet: @testnet),
57
+ signatureChainId: '0x66eee'
58
+ )
59
+
60
+ typed_data = {
61
+ types: {
62
+ EIP712Domain: EIP712.domain_type
63
+ }.merge(sign_types),
64
+ primaryType: primary_type,
65
+ domain: EIP712.user_signed_domain,
66
+ message: message
67
+ }
68
+
69
+ sign_typed_data(typed_data)
70
+ end
71
+
47
72
  private
48
73
 
49
74
  # Normalize private key format
@@ -54,7 +79,7 @@ module Hyperliquid
54
79
  end
55
80
 
56
81
  # Construct the phantom agent for signing
57
- # Maintains parity with Python SDK
82
+ # Maintains parity with official Python SDK
58
83
  # @param action [Hash] Action payload
59
84
  # @param nonce [Integer] Nonce timestamp
60
85
  # @param vault_address [String, nil] Optional vault address
@@ -62,7 +87,7 @@ module Hyperliquid
62
87
  # @return [Hash] Phantom agent with source and connectionId
63
88
  def construct_phantom_agent(action, nonce, vault_address, expires_after)
64
89
  # Compute action hash
65
- # Maintains parity with Python SDK
90
+ # Maintains parity with official Python SDK
66
91
  # data = msgpack(action) + nonce(8 bytes BE) + vault_flag + [vault_addr] + [expires_flag + expires_after]
67
92
  # - Note: expires_flag is only included if expires_after exists. A bit odd but that's what the
68
93
  # Python SDK does.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hyperliquid
4
- VERSION = '0.4.1'
4
+ VERSION = '0.6.0'
5
5
  end
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Test 1: Spot Market Roundtrip (PURR/USDC)
5
+ # Buy and sell PURR at market price.
6
+
7
+ require_relative 'test_helpers'
8
+
9
+ sdk = build_sdk
10
+ separator('TEST 1: Spot Market Roundtrip (PURR/USDC)')
11
+
12
+ spot_coin = 'PURR/USDC'
13
+ spot_size = 5
14
+
15
+ mids = sdk.info.all_mids
16
+ spot_price = mids[spot_coin]&.to_f
17
+
18
+ if spot_price&.positive?
19
+ puts "#{spot_coin} mid: $#{spot_price}"
20
+ puts "Size: #{spot_size} PURR (~$#{(spot_size * spot_price).round(2)})"
21
+ puts "Slippage: #{(SPOT_SLIPPAGE * 100).to_i}%"
22
+ puts
23
+
24
+ puts 'Placing market BUY...'
25
+ result = sdk.exchange.market_order(
26
+ coin: spot_coin,
27
+ is_buy: true,
28
+ size: spot_size,
29
+ slippage: SPOT_SLIPPAGE
30
+ )
31
+ check_result(result, 'Buy')
32
+
33
+ wait_with_countdown(WAIT_SECONDS, 'Waiting before sell...')
34
+
35
+ puts 'Placing market SELL...'
36
+ result = sdk.exchange.market_order(
37
+ coin: spot_coin,
38
+ is_buy: false,
39
+ size: spot_size,
40
+ slippage: SPOT_SLIPPAGE
41
+ )
42
+ check_result(result, 'Sell')
43
+ else
44
+ puts red("SKIPPED: Could not get #{spot_coin} price")
45
+ end
46
+
47
+ test_passed('Test 1 Spot Market Roundtrip')
48
+
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Test 2: Spot Limit Order (Place and Cancel)
5
+ # Place a limit buy well below market, then cancel it.
6
+
7
+ require_relative 'test_helpers'
8
+
9
+ sdk = build_sdk
10
+ separator('TEST 2: Spot Limit Order (Place and Cancel)')
11
+
12
+ spot_coin = 'PURR/USDC'
13
+ spot_size = 5
14
+
15
+ mids = sdk.info.all_mids
16
+ spot_price = mids[spot_coin]&.to_f
17
+
18
+ if spot_price&.positive?
19
+ limit_price = (spot_price * 0.50).round(2)
20
+ puts "#{spot_coin} mid: $#{spot_price}"
21
+ puts "Limit price: $#{limit_price} (50% below mid - won't fill)"
22
+ puts "Size: #{spot_size} PURR"
23
+ puts
24
+
25
+ puts 'Placing limit BUY order...'
26
+ result = sdk.exchange.order(
27
+ coin: spot_coin,
28
+ is_buy: true,
29
+ size: spot_size,
30
+ limit_px: limit_price,
31
+ order_type: { limit: { tif: 'Gtc' } },
32
+ reduce_only: false
33
+ )
34
+ oid = check_result(result, 'Limit order')
35
+
36
+ if oid.is_a?(Integer)
37
+ wait_with_countdown(WAIT_SECONDS, 'Order resting. Waiting before cancel...')
38
+
39
+ puts "Canceling order #{oid}..."
40
+ result = sdk.exchange.cancel(coin: spot_coin, oid: oid)
41
+ check_result(result, 'Cancel')
42
+ end
43
+ else
44
+ puts red("SKIPPED: Could not get #{spot_coin} price")
45
+ end
46
+
47
+ test_passed('Test 2 Spot Limit Order')
48
+
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Test 3: Perp Market Roundtrip (BTC Long)
5
+ # Open a long BTC position, then close it.
6
+
7
+ require_relative 'test_helpers'
8
+
9
+ sdk = build_sdk
10
+ separator('TEST 3: Perp Market Roundtrip (BTC Long)')
11
+
12
+ perp_coin = 'BTC'
13
+ mids = sdk.info.all_mids
14
+ btc_price = mids[perp_coin]&.to_f
15
+
16
+ if btc_price&.positive?
17
+ meta = sdk.info.meta
18
+ btc_meta = meta['universe'].find { |a| a['name'] == perp_coin }
19
+ sz_decimals = btc_meta['szDecimals']
20
+
21
+ perp_size = (20.0 / btc_price).ceil(sz_decimals)
22
+
23
+ puts "#{perp_coin} mid: $#{btc_price.round(2)}"
24
+ puts "Size: #{perp_size} BTC (~$#{(perp_size * btc_price).round(2)})"
25
+ puts "Slippage: #{(PERP_SLIPPAGE * 100).to_i}%"
26
+ puts
27
+
28
+ puts 'Opening LONG position (market buy)...'
29
+ result = sdk.exchange.market_order(
30
+ coin: perp_coin,
31
+ is_buy: true,
32
+ size: perp_size,
33
+ slippage: PERP_SLIPPAGE
34
+ )
35
+ check_result(result, 'Long open')
36
+
37
+ wait_with_countdown(WAIT_SECONDS, 'Position open. Waiting before close...')
38
+
39
+ puts 'Closing LONG position (market sell)...'
40
+ result = sdk.exchange.market_order(
41
+ coin: perp_coin,
42
+ is_buy: false,
43
+ size: perp_size,
44
+ slippage: PERP_SLIPPAGE
45
+ )
46
+ check_result(result, 'Long close')
47
+ else
48
+ puts red("SKIPPED: Could not get #{perp_coin} price")
49
+ end
50
+
51
+ test_passed('Test 3 Perp Market Roundtrip')
52
+