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.
- package/CHANGELOG.md +35 -0
- package/README.md +52 -0
- package/SKILL.md +57 -5
- package/bin/cli.ts +11 -0
- package/dist/auto/examples/grid.js +1 -1
- package/dist/auto/examples/mm-maker.js +4 -4
- package/dist/auto/examples/mm-spread.js +4 -4
- package/dist/core/client.d.ts +83 -32
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +214 -29
- package/dist/core/types.d.ts +22 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/utils.d.ts +29 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +27 -0
- package/dist/core/ws.d.ts +5 -0
- package/dist/core/ws.d.ts.map +1 -1
- package/dist/core/ws.js +9 -7
- package/dist/guardian/cli.d.ts +2 -0
- package/dist/guardian/cli.d.ts.map +1 -0
- package/dist/guardian/cli.js +288 -0
- package/dist/guardian/engine.d.ts +107 -0
- package/dist/guardian/engine.d.ts.map +1 -0
- package/dist/guardian/engine.js +526 -0
- package/dist/guardian/rules.d.ts +35 -0
- package/dist/guardian/rules.d.ts.map +1 -0
- package/dist/guardian/rules.js +237 -0
- package/dist/guardian/telegram.d.ts +24 -0
- package/dist/guardian/telegram.d.ts.map +1 -0
- package/dist/guardian/telegram.js +125 -0
- package/dist/guardian/types.d.ts +90 -0
- package/dist/guardian/types.d.ts.map +1 -0
- package/dist/guardian/types.js +45 -0
- package/dist/lib.d.ts +10 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +6 -1
- package/dist/operations/advanced-orders.test.js +302 -13
- package/dist/operations/bracket.d.ts +35 -5
- package/dist/operations/bracket.d.ts.map +1 -1
- package/dist/operations/bracket.js +238 -64
- package/dist/operations/cancel.js +4 -1
- package/dist/operations/chase.d.ts +4 -2
- package/dist/operations/chase.d.ts.map +1 -1
- package/dist/operations/chase.js +61 -24
- package/dist/operations/scale.d.ts.map +1 -1
- package/dist/operations/scale.js +10 -1
- package/dist/operations/set-tpsl.js +69 -57
- package/dist/operations/trigger-order.js +18 -6
- package/dist/operations/twap.js +13 -1
- package/package.json +2 -2
- package/scripts/auto/examples/grid.ts +1 -1
- package/scripts/auto/examples/mm-maker.ts +4 -4
- package/scripts/auto/examples/mm-spread.ts +4 -4
- package/scripts/core/client.ts +278 -33
- package/scripts/core/types.ts +23 -0
- package/scripts/core/utils.ts +37 -0
- package/scripts/core/ws.ts +9 -6
- package/scripts/guardian/cli.ts +322 -0
- package/scripts/guardian/engine.ts +614 -0
- package/scripts/guardian/rules.ts +265 -0
- package/scripts/guardian/telegram.ts +147 -0
- package/scripts/guardian/types.ts +160 -0
- package/scripts/lib.ts +36 -0
- package/scripts/operations/advanced-orders.test.ts +347 -14
- package/scripts/operations/bracket.ts +252 -73
- package/scripts/operations/cancel.ts +4 -1
- package/scripts/operations/chase.ts +61 -26
- package/scripts/operations/scale.ts +10 -1
- package/scripts/operations/set-tpsl.ts +62 -57
- package/scripts/operations/trigger-order.ts +20 -6
- package/scripts/operations/twap.ts +14 -1
package/scripts/core/client.ts
CHANGED
|
@@ -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
|
-
//
|
|
2626
|
-
// isMarket: false
|
|
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
|
|
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
|
-
//
|
|
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
|
|
2708
|
-
*
|
|
2709
|
-
*
|
|
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
|
|
2746
|
+
async tpslOrders(
|
|
2712
2747
|
coin: string,
|
|
2713
2748
|
isBuy: boolean,
|
|
2714
2749
|
size: number,
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
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 =
|
|
2730
|
-
?
|
|
2731
|
-
:
|
|
2778
|
+
const stopLossLimitPrice = stopLossPrice === undefined
|
|
2779
|
+
? undefined
|
|
2780
|
+
: isBuy
|
|
2781
|
+
? stopLossPrice * (1 + slippageMult)
|
|
2782
|
+
: stopLossPrice * (1 - slippageMult);
|
|
2732
2783
|
|
|
2733
|
-
|
|
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
|
|
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:
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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 }
|
|
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
|
}
|
package/scripts/core/types.ts
CHANGED
|
@@ -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 {
|
package/scripts/core/utils.ts
CHANGED
|
@@ -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
|
*/
|
package/scripts/core/ws.ts
CHANGED
|
@@ -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,
|