openbroker 1.9.6 → 1.12.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +52 -0
  3. package/SKILL.md +57 -5
  4. package/bin/cli.ts +11 -0
  5. package/dist/auto/examples/grid.js +1 -1
  6. package/dist/auto/examples/mm-maker.js +4 -4
  7. package/dist/auto/examples/mm-spread.js +4 -4
  8. package/dist/core/client.d.ts +83 -32
  9. package/dist/core/client.d.ts.map +1 -1
  10. package/dist/core/client.js +214 -29
  11. package/dist/core/types.d.ts +22 -0
  12. package/dist/core/types.d.ts.map +1 -1
  13. package/dist/core/utils.d.ts +29 -0
  14. package/dist/core/utils.d.ts.map +1 -1
  15. package/dist/core/utils.js +27 -0
  16. package/dist/core/ws.d.ts +5 -0
  17. package/dist/core/ws.d.ts.map +1 -1
  18. package/dist/core/ws.js +9 -7
  19. package/dist/guardian/cli.d.ts +2 -0
  20. package/dist/guardian/cli.d.ts.map +1 -0
  21. package/dist/guardian/cli.js +288 -0
  22. package/dist/guardian/engine.d.ts +107 -0
  23. package/dist/guardian/engine.d.ts.map +1 -0
  24. package/dist/guardian/engine.js +526 -0
  25. package/dist/guardian/rules.d.ts +35 -0
  26. package/dist/guardian/rules.d.ts.map +1 -0
  27. package/dist/guardian/rules.js +237 -0
  28. package/dist/guardian/telegram.d.ts +24 -0
  29. package/dist/guardian/telegram.d.ts.map +1 -0
  30. package/dist/guardian/telegram.js +125 -0
  31. package/dist/guardian/types.d.ts +90 -0
  32. package/dist/guardian/types.d.ts.map +1 -0
  33. package/dist/guardian/types.js +45 -0
  34. package/dist/lib.d.ts +10 -1
  35. package/dist/lib.d.ts.map +1 -1
  36. package/dist/lib.js +6 -1
  37. package/dist/operations/advanced-orders.test.js +302 -13
  38. package/dist/operations/bracket.d.ts +35 -5
  39. package/dist/operations/bracket.d.ts.map +1 -1
  40. package/dist/operations/bracket.js +238 -64
  41. package/dist/operations/cancel.js +4 -1
  42. package/dist/operations/chase.d.ts +4 -2
  43. package/dist/operations/chase.d.ts.map +1 -1
  44. package/dist/operations/chase.js +61 -24
  45. package/dist/operations/scale.d.ts.map +1 -1
  46. package/dist/operations/scale.js +10 -1
  47. package/dist/operations/set-tpsl.js +69 -57
  48. package/dist/operations/trigger-order.js +18 -6
  49. package/dist/operations/twap.js +13 -1
  50. package/package.json +2 -2
  51. package/scripts/auto/examples/grid.ts +1 -1
  52. package/scripts/auto/examples/mm-maker.ts +4 -4
  53. package/scripts/auto/examples/mm-spread.ts +4 -4
  54. package/scripts/core/client.ts +278 -33
  55. package/scripts/core/types.ts +23 -0
  56. package/scripts/core/utils.ts +37 -0
  57. package/scripts/core/ws.ts +9 -6
  58. package/scripts/guardian/cli.ts +322 -0
  59. package/scripts/guardian/engine.ts +614 -0
  60. package/scripts/guardian/rules.ts +265 -0
  61. package/scripts/guardian/telegram.ts +147 -0
  62. package/scripts/guardian/types.ts +160 -0
  63. package/scripts/lib.ts +36 -0
  64. package/scripts/operations/advanced-orders.test.ts +347 -14
  65. package/scripts/operations/bracket.ts +252 -73
  66. package/scripts/operations/cancel.ts +4 -1
  67. package/scripts/operations/chase.ts +61 -26
  68. package/scripts/operations/scale.ts +10 -1
  69. package/scripts/operations/set-tpsl.ts +62 -57
  70. package/scripts/operations/trigger-order.ts +20 -6
  71. package/scripts/operations/twap.ts +14 -1
@@ -14,12 +14,13 @@ import type {
14
14
  ClearinghouseState,
15
15
  MarginSummary,
16
16
  OpenOrder,
17
+ FrontendOpenOrder,
17
18
  OutcomeMetaResponse,
18
19
  OutcomeMarket,
19
20
  OutcomeQuestion,
20
21
  } from './types.js';
21
22
  import { loadConfig, isMainnet } from './config.js';
22
- import { roundPrice, roundSize } from './utils.js';
23
+ import { MIN_ORDER_NOTIONAL_USD, roundPrice, roundSize } from './utils.js';
23
24
 
24
25
  export interface RealtimeBookSnapshot {
25
26
  coin: string;
@@ -2350,6 +2351,22 @@ export class HyperliquidClient {
2350
2351
  return orders;
2351
2352
  }
2352
2353
 
2354
+ /**
2355
+ * Open orders with frontend display fields (isTrigger, reduceOnly,
2356
+ * isPositionTpsl, …) for one dex — empty/omitted dex = main. Unlike
2357
+ * getOpenOrders this does NOT aggregate HIP-3 dexes; callers that need a
2358
+ * HIP-3 book pass its dex name explicitly.
2359
+ */
2360
+ async getFrontendOpenOrders(user?: string, dex?: string): Promise<FrontendOpenOrder[]> {
2361
+ const target = (user ?? this.address) as `0x${string}`;
2362
+ this.log('Fetching frontendOpenOrders for:', target, dex ? `(dex: ${dex})` : '');
2363
+ const response = await this.withRetry(
2364
+ () => this.info.frontendOpenOrders(dex ? { user: target, dex } : { user: target }),
2365
+ 'frontendOpenOrders',
2366
+ );
2367
+ return response as unknown as FrontendOpenOrder[];
2368
+ }
2369
+
2353
2370
  // ============ Trading ============
2354
2371
 
2355
2372
  /**
@@ -2596,6 +2613,8 @@ export class HyperliquidClient {
2596
2613
  * @param limitPrice - Limit price for the order (use triggerPrice for market-like execution)
2597
2614
  * @param tpsl - 'tp' for take profit, 'sl' for stop loss
2598
2615
  * @param reduceOnly - Whether order is reduce-only (should be true for TP/SL)
2616
+ * @param isMarket - Execute as a market trigger on fire; limitPrice then only
2617
+ * caps the fill (slippage band) instead of resting on the book
2599
2618
  */
2600
2619
  async triggerOrder(
2601
2620
  coin: string,
@@ -2605,7 +2624,8 @@ export class HyperliquidClient {
2605
2624
  limitPrice: number,
2606
2625
  tpsl: 'tp' | 'sl',
2607
2626
  reduceOnly: boolean = true,
2608
- leverage?: number
2627
+ leverage?: number,
2628
+ isMarket: boolean = false
2609
2629
  ): Promise<OrderResponse> {
2610
2630
  await this.requireTrading();
2611
2631
  await this.getMetaAndAssetCtxs();
@@ -2622,9 +2642,8 @@ export class HyperliquidClient {
2622
2642
  const assetIndex = this.getAssetIndex(coin);
2623
2643
  const szDecimals = this.getSzDecimals(coin);
2624
2644
 
2625
- // For trigger orders, we use the trigger order type
2626
- // isMarket: false means it becomes a limit order at limitPrice when triggered
2627
- // For stop loss, we typically want some slippage protection
2645
+ // isMarket: true fires as a market order capped by limitPrice (slippage band);
2646
+ // isMarket: false rests as a limit order at limitPrice once triggered.
2628
2647
  const orderWire = {
2629
2648
  a: assetIndex,
2630
2649
  b: isBuy,
@@ -2634,7 +2653,7 @@ export class HyperliquidClient {
2634
2653
  t: {
2635
2654
  trigger: {
2636
2655
  triggerPx: roundPrice(triggerPrice, szDecimals),
2637
- isMarket: false,
2656
+ isMarket,
2638
2657
  tpsl,
2639
2658
  },
2640
2659
  },
@@ -2671,23 +2690,30 @@ export class HyperliquidClient {
2671
2690
  }
2672
2691
 
2673
2692
  /**
2674
- * Place a stop loss order
2693
+ * Place a stop loss order.
2694
+ *
2695
+ * Executes as a market trigger by default: in a gap move that jumps past the
2696
+ * trigger, a stop-limit's band can be skipped entirely and the position sits
2697
+ * unprotected — the exact scenario an SL exists for. The limit price still
2698
+ * caps the fill at `slippageBps` past the trigger. Pass `isMarket: false`
2699
+ * for the stop-limit variant (rests at the band price once triggered).
2675
2700
  */
2676
2701
  async stopLoss(
2677
2702
  coin: string,
2678
2703
  isBuy: boolean,
2679
2704
  size: number,
2680
2705
  triggerPrice: number,
2681
- slippageBps: number = 100 // 1% slippage for SL execution
2706
+ slippageBps: number = 100, // 1% slippage cap for SL execution
2707
+ isMarket: boolean = true
2682
2708
  ): Promise<OrderResponse> {
2683
- // For stop loss, limit price should be worse than trigger to ensure fill
2709
+ // Limit price sits worse than trigger to ensure fill
2684
2710
  // Buy SL: limit above trigger, Sell SL: limit below trigger
2685
2711
  const slippageMult = slippageBps / 10000;
2686
2712
  const limitPrice = isBuy
2687
2713
  ? triggerPrice * (1 + slippageMult)
2688
2714
  : triggerPrice * (1 - slippageMult);
2689
2715
 
2690
- return this.triggerOrder(coin, isBuy, size, triggerPrice, limitPrice, 'sl', true);
2716
+ return this.triggerOrder(coin, isBuy, size, triggerPrice, limitPrice, 'sl', true, undefined, isMarket);
2691
2717
  }
2692
2718
 
2693
2719
  /**
@@ -2704,19 +2730,42 @@ export class HyperliquidClient {
2704
2730
  }
2705
2731
 
2706
2732
  /**
2707
- * Place a paired TP/SL trigger set using Hyperliquid's normalTpsl grouping.
2708
- * The two orders are submitted together so the venue treats them as a linked
2709
- * bracket pair instead of two unrelated reduce-only triggers.
2733
+ * Place TP and/or SL triggers for an open position in one batch.
2734
+ *
2735
+ * Defaults to Hyperliquid's `positionTpsl` grouping: the triggers are tied
2736
+ * to the open position (cancelled when it closes, OCO between themselves)
2737
+ * — the same mechanism the Hyperliquid frontend uses for position TP/SL.
2738
+ * `normalTpsl` is available for a standalone OCO pair not bound to the
2739
+ * position. SL executes as a market trigger by default with the limit price
2740
+ * capping the fill `slSlippageBps` past the trigger (see `stopLoss`).
2741
+ *
2742
+ * `isBuy` is the EXIT side (opposite of the position direction). Statuses
2743
+ * in the response align with the orders sent: TP first (when present),
2744
+ * then SL.
2710
2745
  */
2711
- async tpslPair(
2746
+ async tpslOrders(
2712
2747
  coin: string,
2713
2748
  isBuy: boolean,
2714
2749
  size: number,
2715
- takeProfitPrice: number,
2716
- stopLossPrice: number,
2717
- stopLossSlippageBps: number = 100,
2718
- leverage?: number
2750
+ opts: {
2751
+ takeProfitPrice?: number;
2752
+ stopLossPrice?: number;
2753
+ stopLossSlippageBps?: number;
2754
+ /** SL fires as a market trigger (default true); false = stop-limit. */
2755
+ stopLossIsMarket?: boolean;
2756
+ grouping?: 'positionTpsl' | 'normalTpsl';
2757
+ leverage?: number;
2758
+ } = {}
2719
2759
  ): Promise<OrderResponse> {
2760
+ const { takeProfitPrice, stopLossPrice, leverage } = opts;
2761
+ const stopLossSlippageBps = opts.stopLossSlippageBps ?? 100;
2762
+ const stopLossIsMarket = opts.stopLossIsMarket ?? true;
2763
+ const grouping = opts.grouping ?? 'positionTpsl';
2764
+
2765
+ if (takeProfitPrice === undefined && stopLossPrice === undefined) {
2766
+ throw new Error('tpslOrders requires takeProfitPrice, stopLossPrice, or both');
2767
+ }
2768
+
2720
2769
  await this.requireTrading();
2721
2770
  await this.getMetaAndAssetCtxs();
2722
2771
 
@@ -2726,18 +2775,22 @@ export class HyperliquidClient {
2726
2775
  }
2727
2776
 
2728
2777
  const slippageMult = stopLossSlippageBps / 10000;
2729
- const stopLossLimitPrice = isBuy
2730
- ? stopLossPrice * (1 + slippageMult)
2731
- : stopLossPrice * (1 - slippageMult);
2778
+ const stopLossLimitPrice = stopLossPrice === undefined
2779
+ ? undefined
2780
+ : isBuy
2781
+ ? stopLossPrice * (1 + slippageMult)
2782
+ : stopLossPrice * (1 - slippageMult);
2732
2783
 
2733
- await this.ensureHip3Ready(coin, size * Math.max(takeProfitPrice, stopLossLimitPrice), leverage);
2784
+ const worstPrice = Math.max(takeProfitPrice ?? 0, stopLossLimitPrice ?? 0);
2785
+ await this.ensureHip3Ready(coin, size * worstPrice, leverage);
2734
2786
 
2735
2787
  const assetIndex = this.getAssetIndex(coin);
2736
2788
  const szDecimals = this.getSzDecimals(coin);
2737
2789
  const roundedSize = roundSize(size, szDecimals);
2738
2790
 
2739
- const orderWires = [
2740
- {
2791
+ const orderWires = [];
2792
+ if (takeProfitPrice !== undefined) {
2793
+ orderWires.push({
2741
2794
  a: assetIndex,
2742
2795
  b: isBuy,
2743
2796
  p: roundPrice(takeProfitPrice, szDecimals),
@@ -2750,22 +2803,182 @@ export class HyperliquidClient {
2750
2803
  tpsl: 'tp' as const,
2751
2804
  },
2752
2805
  },
2753
- },
2754
- {
2806
+ });
2807
+ }
2808
+ if (stopLossPrice !== undefined) {
2809
+ orderWires.push({
2755
2810
  a: assetIndex,
2756
2811
  b: isBuy,
2757
- p: roundPrice(stopLossLimitPrice, szDecimals),
2812
+ p: roundPrice(stopLossLimitPrice!, szDecimals),
2758
2813
  s: roundedSize,
2759
2814
  r: true,
2760
2815
  t: {
2761
2816
  trigger: {
2762
2817
  triggerPx: roundPrice(stopLossPrice, szDecimals),
2763
- isMarket: false,
2818
+ isMarket: stopLossIsMarket,
2764
2819
  tpsl: 'sl' as const,
2765
2820
  },
2766
2821
  },
2822
+ });
2823
+ }
2824
+
2825
+ const orderRequest: {
2826
+ orders: typeof orderWires;
2827
+ grouping: 'positionTpsl' | 'normalTpsl';
2828
+ builder?: BuilderInfo;
2829
+ } = {
2830
+ orders: orderWires,
2831
+ grouping,
2832
+ };
2833
+
2834
+ if (!this.isTestnet && this.config.builderAddress !== '0x0000000000000000000000000000000000000000') {
2835
+ orderRequest.builder = this.builderInfo;
2836
+ this.log('Including builder fee:', this.builderInfo);
2837
+ }
2838
+
2839
+ try {
2840
+ const response = await this.exchange.order(orderRequest, this.vaultParam);
2841
+ this.log('TP/SL orders response:', JSON.stringify(response, null, 2));
2842
+ return response as unknown as OrderResponse;
2843
+ } catch (error) {
2844
+ this.log('TP/SL orders error:', error);
2845
+ return {
2846
+ status: 'err',
2847
+ response: error instanceof Error ? error.message : String(error),
2848
+ };
2849
+ }
2850
+ }
2851
+
2852
+ /**
2853
+ * Place a paired TP/SL trigger set for an open position.
2854
+ *
2855
+ * Back-compat wrapper over `tpslOrders`. Since v1.10.0 this uses the
2856
+ * `positionTpsl` grouping (triggers track the open position and OCO-cancel)
2857
+ * instead of a standalone `normalTpsl` pair, and the SL fires as a market
2858
+ * trigger capped by the slippage band instead of a stop-limit.
2859
+ */
2860
+ async tpslPair(
2861
+ coin: string,
2862
+ isBuy: boolean,
2863
+ size: number,
2864
+ takeProfitPrice: number,
2865
+ stopLossPrice: number,
2866
+ stopLossSlippageBps: number = 100,
2867
+ leverage?: number
2868
+ ): Promise<OrderResponse> {
2869
+ return this.tpslOrders(coin, isBuy, size, {
2870
+ takeProfitPrice,
2871
+ stopLossPrice,
2872
+ stopLossSlippageBps,
2873
+ leverage,
2874
+ });
2875
+ }
2876
+
2877
+ /**
2878
+ * Atomic bracket: a limit entry with TP/SL children in one `normalTpsl`
2879
+ * batch — exactly how the Hyperliquid frontend does order-attached TP/SL.
2880
+ * The children arm only when the entry fills and are sized to it; their
2881
+ * statuses come back as the plain strings "waitingForFill" /
2882
+ * "waitingForTrigger" (parse with `parseOrderStatus`).
2883
+ *
2884
+ * `isBuy` is the ENTRY side; exits are placed on the opposite side,
2885
+ * reduce-only. Statuses align with [entry, tp?, sl?].
2886
+ */
2887
+ async bracketOrder(
2888
+ coin: string,
2889
+ isBuy: boolean,
2890
+ size: number,
2891
+ entryPrice: number,
2892
+ opts: {
2893
+ entryTif?: 'Gtc' | 'Alo';
2894
+ takeProfitPrice?: number;
2895
+ stopLossPrice?: number;
2896
+ stopLossSlippageBps?: number;
2897
+ /** SL fires as a market trigger (default true); false = stop-limit. */
2898
+ stopLossIsMarket?: boolean;
2899
+ leverage?: number;
2900
+ } = {}
2901
+ ): Promise<OrderResponse> {
2902
+ const { takeProfitPrice, stopLossPrice, leverage } = opts;
2903
+ const entryTif = opts.entryTif ?? 'Gtc';
2904
+ const stopLossSlippageBps = opts.stopLossSlippageBps ?? 100;
2905
+ const stopLossIsMarket = opts.stopLossIsMarket ?? true;
2906
+
2907
+ if (takeProfitPrice === undefined && stopLossPrice === undefined) {
2908
+ throw new Error('bracketOrder requires takeProfitPrice, stopLossPrice, or both');
2909
+ }
2910
+
2911
+ await this.requireTrading();
2912
+ await this.getMetaAndAssetCtxs();
2913
+
2914
+ if (leverage && !this.isHip3(coin)) {
2915
+ this.log(`Setting leverage for ${coin} to ${leverage}x cross`);
2916
+ await this.updateLeverage(coin, leverage, true);
2917
+ }
2918
+
2919
+ await this.ensureHip3Ready(coin, size * entryPrice, leverage);
2920
+
2921
+ const assetIndex = this.getAssetIndex(coin);
2922
+ const szDecimals = this.getSzDecimals(coin);
2923
+ const roundedSize = roundSize(size, szDecimals);
2924
+ const exitBuy = !isBuy;
2925
+
2926
+ const slippageMult = stopLossSlippageBps / 10000;
2927
+ const stopLossLimitPrice = stopLossPrice === undefined
2928
+ ? undefined
2929
+ : exitBuy
2930
+ ? stopLossPrice * (1 + slippageMult)
2931
+ : stopLossPrice * (1 - slippageMult);
2932
+
2933
+ const orderWires: Array<{
2934
+ a: number;
2935
+ b: boolean;
2936
+ p: string;
2937
+ s: string;
2938
+ r: boolean;
2939
+ t: { limit: { tif: 'Gtc' | 'Alo' } } | { trigger: { triggerPx: string; isMarket: boolean; tpsl: 'tp' | 'sl' } };
2940
+ }> = [
2941
+ {
2942
+ a: assetIndex,
2943
+ b: isBuy,
2944
+ p: roundPrice(entryPrice, szDecimals),
2945
+ s: roundedSize,
2946
+ r: false,
2947
+ t: { limit: { tif: entryTif } },
2767
2948
  },
2768
2949
  ];
2950
+ if (takeProfitPrice !== undefined) {
2951
+ orderWires.push({
2952
+ a: assetIndex,
2953
+ b: exitBuy,
2954
+ p: roundPrice(takeProfitPrice, szDecimals),
2955
+ s: roundedSize,
2956
+ r: true,
2957
+ t: {
2958
+ trigger: {
2959
+ triggerPx: roundPrice(takeProfitPrice, szDecimals),
2960
+ isMarket: false,
2961
+ tpsl: 'tp',
2962
+ },
2963
+ },
2964
+ });
2965
+ }
2966
+ if (stopLossPrice !== undefined) {
2967
+ orderWires.push({
2968
+ a: assetIndex,
2969
+ b: exitBuy,
2970
+ p: roundPrice(stopLossLimitPrice!, szDecimals),
2971
+ s: roundedSize,
2972
+ r: true,
2973
+ t: {
2974
+ trigger: {
2975
+ triggerPx: roundPrice(stopLossPrice, szDecimals),
2976
+ isMarket: stopLossIsMarket,
2977
+ tpsl: 'sl',
2978
+ },
2979
+ },
2980
+ });
2981
+ }
2769
2982
 
2770
2983
  const orderRequest: {
2771
2984
  orders: typeof orderWires;
@@ -2783,10 +2996,10 @@ export class HyperliquidClient {
2783
2996
 
2784
2997
  try {
2785
2998
  const response = await this.exchange.order(orderRequest, this.vaultParam);
2786
- this.log('TP/SL pair response:', JSON.stringify(response, null, 2));
2999
+ this.log('Bracket order response:', JSON.stringify(response, null, 2));
2787
3000
  return response as unknown as OrderResponse;
2788
3001
  } catch (error) {
2789
- this.log('TP/SL pair error:', error);
3002
+ this.log('Bracket order error:', error);
2790
3003
  return {
2791
3004
  status: 'err',
2792
3005
  response: error instanceof Error ? error.message : String(error),
@@ -2794,17 +3007,24 @@ export class HyperliquidClient {
2794
3007
  }
2795
3008
  }
2796
3009
 
2797
- async cancel(coin: string, oid: number): Promise<CancelResponse> {
3010
+ /**
3011
+ * `fast: true` sets the action's `f` flag so the mempool prioritizes the cancel
3012
+ * (post-2026-07 upgrade, only fast cancels get prioritization). The API REJECTS
3013
+ * fast cancels for trigger orders (TP/SL), so only pass it when the oid is known
3014
+ * to be a plain resting limit — e.g. a quoting loop cancelling its own quotes.
3015
+ */
3016
+ async cancel(coin: string, oid: number, opts?: { fast?: boolean }): Promise<CancelResponse> {
2798
3017
  await this.requireTrading();
2799
3018
  await this.getMetaAndAssetCtxs();
2800
3019
 
2801
3020
  const assetIndex = this.getAssetIndex(coin);
2802
3021
 
2803
- this.log(`Cancelling order: ${coin} (asset ${assetIndex}) oid ${oid}`);
3022
+ this.log(`Cancelling order: ${coin} (asset ${assetIndex}) oid ${oid}${opts?.fast ? ' (fast)' : ''}`);
2804
3023
 
2805
3024
  try {
2806
3025
  const response = await this.exchange.cancel({
2807
3026
  cancels: [{ a: assetIndex, o: oid }],
3027
+ ...(opts?.fast ? { f: true as const } : {}),
2808
3028
  }, this.vaultParam);
2809
3029
  this.log('Cancel response:', JSON.stringify(response, null, 2));
2810
3030
  return response as unknown as CancelResponse;
@@ -2820,14 +3040,16 @@ export class HyperliquidClient {
2820
3040
  /**
2821
3041
  * Cancel MANY resting orders in a SINGLE exchange request (one action-rate request for ≤40 cancels),
2822
3042
  * the counterpart to `bulkOrder`. `response.data.statuses[i]` aligns with `cancels[i]`.
3043
+ * `fast` applies to the whole action and is rejected for trigger orders — see cancel().
2823
3044
  */
2824
- async bulkCancel(cancels: Array<{ coin: string; oid: number }>): Promise<CancelResponse> {
3045
+ async bulkCancel(cancels: Array<{ coin: string; oid: number }>, opts?: { fast?: boolean }): Promise<CancelResponse> {
2825
3046
  await this.requireTrading();
2826
3047
  await this.getMetaAndAssetCtxs();
2827
3048
  if (cancels.length === 0) return { status: 'ok', response: { type: 'cancel', data: { statuses: [] } } } as unknown as CancelResponse;
2828
3049
  try {
2829
3050
  const response = await this.exchange.cancel({
2830
3051
  cancels: cancels.map((c) => ({ a: this.getAssetIndex(c.coin), o: c.oid })),
3052
+ ...(opts?.fast ? { f: true as const } : {}),
2831
3053
  }, this.vaultParam);
2832
3054
  this.log('Bulk cancel response:', JSON.stringify(response, null, 2));
2833
3055
  return response as unknown as CancelResponse;
@@ -3247,6 +3469,29 @@ export class HyperliquidClient {
3247
3469
  ) {
3248
3470
  await this.getMetaAndAssetCtxs();
3249
3471
 
3472
+ if (!Number.isFinite(durationMinutes) || durationMinutes < 5 || durationMinutes > 1440) {
3473
+ throw new Error('TWAP duration must be between 5 and 1440 minutes');
3474
+ }
3475
+
3476
+ // Native TWAP fires a sub-order every 30s; each must clear the exchange
3477
+ // minimum notional or the venue silently skips slices.
3478
+ if (!reduceOnly) {
3479
+ try {
3480
+ const mid = parseFloat((await this.getAllMids())[coin]);
3481
+ if (Number.isFinite(mid) && mid > 0) {
3482
+ const perSlice = (size * mid) / Math.max(1, Math.round(durationMinutes) * 2);
3483
+ if (perSlice < MIN_ORDER_NOTIONAL_USD) {
3484
+ throw new Error(
3485
+ `TWAP slices of ~$${perSlice.toFixed(2)} fall below the $${MIN_ORDER_NOTIONAL_USD} exchange minimum — shorten the duration or increase the size`
3486
+ );
3487
+ }
3488
+ }
3489
+ } catch (error) {
3490
+ if (error instanceof Error && error.message.includes('exchange minimum')) throw error;
3491
+ this.log('TWAP min-notional pre-check skipped (no mid available):', error);
3492
+ }
3493
+ }
3494
+
3250
3495
  if (leverage) {
3251
3496
  await this.updateLeverage(coin, leverage);
3252
3497
  }
@@ -241,6 +241,29 @@ export interface OpenOrder {
241
241
  timestamp: number;
242
242
  }
243
243
 
244
+ /**
245
+ * Open order from the `frontendOpenOrders` info endpoint — same as OpenOrder
246
+ * plus the trigger/reduce-only display fields (needed to distinguish resting
247
+ * TP/SL protection from plain limit orders).
248
+ */
249
+ export interface FrontendOpenOrder {
250
+ coin: string;
251
+ side: 'B' | 'A';
252
+ limitPx: string;
253
+ sz: string;
254
+ oid: number;
255
+ timestamp: number;
256
+ origSz: string;
257
+ triggerCondition: string;
258
+ isTrigger: boolean;
259
+ triggerPx: string;
260
+ isPositionTpsl: boolean;
261
+ reduceOnly: boolean;
262
+ orderType: string;
263
+ tif: string | null;
264
+ cloid: string | null;
265
+ }
266
+
244
267
  // ============ API Request/Response ============
245
268
 
246
269
  export interface InfoRequest {
@@ -138,6 +138,43 @@ export function sleep(ms: number): Promise<void> {
138
138
  return new Promise(resolve => setTimeout(resolve, ms));
139
139
  }
140
140
 
141
+ /** Hyperliquid rejects orders below this notional (waived for reduce-only). */
142
+ export const MIN_ORDER_NOTIONAL_USD = 10;
143
+
144
+ export type ParsedOrderStatus =
145
+ | { kind: 'resting'; oid: number }
146
+ | { kind: 'filled'; totalSz: number; avgPx: number; oid: number }
147
+ /** normalTpsl children behind an unfilled parent — success states, not errors. */
148
+ | { kind: 'waiting'; state: 'waitingForFill' | 'waitingForTrigger' }
149
+ | { kind: 'error'; error: string }
150
+ | { kind: 'unknown'; raw: unknown };
151
+
152
+ /**
153
+ * Parse a single entry of an order response's `statuses` array. Handles the
154
+ * plain-string success states ("waitingForFill" / "waitingForTrigger") that
155
+ * Hyperliquid returns for normalTpsl children armed behind an unfilled parent,
156
+ * alongside the usual resting/filled/error objects.
157
+ */
158
+ export function parseOrderStatus(status: unknown): ParsedOrderStatus {
159
+ if (status === 'waitingForFill' || status === 'waitingForTrigger') {
160
+ return { kind: 'waiting', state: status };
161
+ }
162
+ if (status && typeof status === 'object') {
163
+ const data = status as Record<string, unknown>;
164
+ if (data.resting) {
165
+ return { kind: 'resting', oid: (data.resting as { oid: number }).oid };
166
+ }
167
+ if (data.filled) {
168
+ const f = data.filled as { totalSz: string; avgPx: string; oid: number };
169
+ return { kind: 'filled', totalSz: parseFloat(f.totalSz), avgPx: parseFloat(f.avgPx), oid: f.oid };
170
+ }
171
+ if (data.error) {
172
+ return { kind: 'error', error: String(data.error) };
173
+ }
174
+ }
175
+ return { kind: 'unknown', raw: status };
176
+ }
177
+
141
178
  /**
142
179
  * Generate a random client order ID
143
180
  */
@@ -251,12 +251,10 @@ export class WebSocketManager {
251
251
  }
252
252
 
253
253
  private trackSub(sub: ISubscription): ISubscription {
254
+ // SDK ≥0.33 dropped ISubscription.failureSignal, so a sub that dies after a
255
+ // failed resubscribe stays in this list; close() swallows unsubscribe errors,
256
+ // so a dead handle there is harmless.
254
257
  this.subscriptions.push(sub);
255
- sub.failureSignal.addEventListener('abort', () => {
256
- this.log('Subscription failed, removing from tracked list');
257
- const idx = this.subscriptions.indexOf(sub);
258
- if (idx >= 0) this.subscriptions.splice(idx, 1);
259
- });
260
258
  return sub;
261
259
  }
262
260
 
@@ -275,10 +273,15 @@ export class WebSocketManager {
275
273
 
276
274
  /**
277
275
  * Subscribe to L2 order book snapshots for a specific coin.
276
+ *
277
+ * `fast: true` = 5 levels every 0.5s; without it the API degrades to 20 levels
278
+ * every 5s (per the 2026-07 network-upgrade announcement). Every consumer here
279
+ * reads top-of-book only (chase / mm quoting / mid fallback), so fast wins;
280
+ * anything needing depth should use the REST l2Book, which stays full-depth.
278
281
  */
279
282
  async subscribeL2Book(coin: string): Promise<ISubscription> {
280
283
  const client = this.ensureClient();
281
- const sub = await client.l2Book({ coin }, (data: L2BookWsEvent) => {
284
+ const sub = await client.l2Book({ coin, fast: true }, (data: L2BookWsEvent) => {
282
285
  this.emit('l2Book', {
283
286
  coin: data.coin,
284
287
  time: data.time,