openbroker 1.9.5 → 1.9.6

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.
@@ -0,0 +1,106 @@
1
+ import { WebSocketManager } from '../core/ws.js';
2
+ import { sleep } from '../core/utils.js';
3
+ export class UserFillWatcher {
4
+ client;
5
+ fills = new Map();
6
+ seenFills = new Set();
7
+ ws;
8
+ ownsWs;
9
+ started = false;
10
+ sinceMs;
11
+ user;
12
+ onFill = (fill) => {
13
+ this.recordFill({
14
+ oid: fill.oid,
15
+ coin: fill.coin,
16
+ px: fill.px,
17
+ sz: fill.sz,
18
+ time: fill.time,
19
+ });
20
+ };
21
+ constructor(client, options = {}) {
22
+ this.client = client;
23
+ this.ws = options.ws === undefined ? new WebSocketManager(client.verbose) : options.ws;
24
+ this.ownsWs = options.ws === undefined;
25
+ this.sinceMs = options.sinceMs ?? Date.now();
26
+ this.user = (options.user ?? client.address);
27
+ }
28
+ async start() {
29
+ if (this.started)
30
+ return;
31
+ this.started = true;
32
+ if (!this.ws)
33
+ return;
34
+ try {
35
+ await this.ws.connect();
36
+ this.ws.on('userFill', this.onFill);
37
+ await this.ws.subscribeUserFills(this.user);
38
+ }
39
+ catch {
40
+ this.ws.off('userFill', this.onFill);
41
+ if (this.ownsWs)
42
+ await this.ws.close().catch(() => { });
43
+ this.ws = null;
44
+ }
45
+ }
46
+ async stop() {
47
+ if (!this.started)
48
+ return;
49
+ this.started = false;
50
+ if (!this.ws)
51
+ return;
52
+ this.ws.off('userFill', this.onFill);
53
+ if (this.ownsWs)
54
+ await this.ws.close().catch(() => { });
55
+ }
56
+ getFilled(oid) {
57
+ return this.fills.get(oid) ?? { size: 0, notional: 0 };
58
+ }
59
+ async waitForFill(oid, targetSize, timeoutMs, options = {}) {
60
+ const deadline = Date.now() + Math.max(0, timeoutMs);
61
+ const pollMs = Math.max(250, options.pollMs ?? 1000);
62
+ while (Date.now() <= deadline) {
63
+ await this.refreshRestFills(options.coin);
64
+ const filled = this.getFilled(oid);
65
+ if (filled.size >= targetSize * 0.999)
66
+ return filled;
67
+ await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
68
+ }
69
+ await this.refreshRestFills(options.coin);
70
+ return this.getFilled(oid);
71
+ }
72
+ async refreshRestFills(coin) {
73
+ try {
74
+ const fills = await this.client.getUserFills(this.user);
75
+ for (const fill of fills) {
76
+ this.recordFill(fill, coin);
77
+ }
78
+ }
79
+ catch {
80
+ // WebSocket is the primary path; REST polling is best-effort fallback.
81
+ }
82
+ }
83
+ recordFill(fill, expectedCoin) {
84
+ if (expectedCoin && fill.coin !== expectedCoin)
85
+ return;
86
+ if (fill.time < this.sinceMs)
87
+ return;
88
+ const key = `${fill.oid}:${fill.time}:${fill.px}:${fill.sz}`;
89
+ if (this.seenFills.has(key))
90
+ return;
91
+ const size = parseFloat(fill.sz);
92
+ const price = parseFloat(fill.px);
93
+ if (!Number.isFinite(size) || size <= 0 || !Number.isFinite(price) || price <= 0)
94
+ return;
95
+ this.seenFills.add(key);
96
+ const prev = this.fills.get(fill.oid) ?? { size: 0, notional: 0 };
97
+ const next = {
98
+ size: prev.size + size,
99
+ notional: prev.notional + size * price,
100
+ };
101
+ this.fills.set(fill.oid, {
102
+ ...next,
103
+ avgPrice: next.notional / next.size,
104
+ });
105
+ }
106
+ }
@@ -1,3 +1,52 @@
1
1
  #!/usr/bin/env npx tsx
2
- export {};
2
+ import type { CancelResponse, OrderResponse } from '../core/types.js';
3
+ export interface OrderLevel {
4
+ level: number;
5
+ price: number;
6
+ size: number;
7
+ distanceFromMid: number;
8
+ }
9
+ export interface ScaleClient {
10
+ verbose: boolean;
11
+ getAllMids(): Promise<Record<string, string>>;
12
+ bulkOrder(orders: Array<{
13
+ coin: string;
14
+ isBuy: boolean;
15
+ size: number;
16
+ price: number;
17
+ tif?: 'Gtc' | 'Alo';
18
+ reduceOnly?: boolean;
19
+ leverage?: number;
20
+ }>): Promise<OrderResponse>;
21
+ bulkCancel(cancels: Array<{
22
+ coin: string;
23
+ oid: number;
24
+ }>): Promise<CancelResponse>;
25
+ }
26
+ export interface ScaleOptions {
27
+ coin: string;
28
+ side: 'buy' | 'sell';
29
+ size: number;
30
+ levels: number;
31
+ rangePct: number;
32
+ distribution?: 'linear' | 'exponential' | 'flat';
33
+ leverage?: number;
34
+ reduceOnly?: boolean;
35
+ tif?: 'Gtc' | 'Alo';
36
+ dryRun?: boolean;
37
+ verbose?: boolean;
38
+ rollbackOnPartial?: boolean;
39
+ client?: ScaleClient;
40
+ output?: (line: string) => void;
41
+ }
42
+ export interface ScaleResult {
43
+ status: 'dry' | 'complete' | 'partial' | 'failed';
44
+ levels: OrderLevel[];
45
+ restingOids: number[];
46
+ filledOids: number[];
47
+ errors: string[];
48
+ rolledBack: boolean;
49
+ }
50
+ export declare function calculateLevels(midPrice: number, isBuy: boolean, totalSize: number, numLevels: number, rangePct: number, distribution: 'linear' | 'exponential' | 'flat'): OrderLevel[];
51
+ export declare function runScale(opts: ScaleOptions): Promise<ScaleResult>;
3
52
  //# sourceMappingURL=scale.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scale.d.ts","sourceRoot":"","sources":["../../scripts/operations/scale.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"scale.d.ts","sourceRoot":"","sources":["../../scripts/operations/scale.ts"],"names":[],"mappings":";AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAyCtE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,SAAS,CACP,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GACzI,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1B,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACpF;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,QAAQ,GAAG,aAAa,GAAG,MAAM,CAAC;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAClD,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,QAAQ,GAAG,aAAa,GAAG,MAAM,GAC9C,UAAU,EAAE,CA0Cd;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA6HvE"}
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env npx tsx
2
2
  // Scale In/Out - Place a grid of limit orders
3
+ import { fileURLToPath } from 'url';
3
4
  import { getClient } from '../core/client.js';
4
- import { formatUsd, parseArgs, sleep } from '../core/utils.js';
5
+ import { formatUsd, parseArgs } from '../core/utils.js';
5
6
  function printUsage() {
6
7
  console.log(`
7
8
  Open Broker - Scale In/Out
@@ -39,7 +40,7 @@ Examples:
39
40
  npx tsx scripts/operations/scale.ts --coin ETH --side buy --size 2 --levels 8 --range 5 --distribution exponential
40
41
  `);
41
42
  }
42
- function calculateLevels(midPrice, isBuy, totalSize, numLevels, rangePct, distribution) {
43
+ export function calculateLevels(midPrice, isBuy, totalSize, numLevels, rangePct, distribution) {
43
44
  const levels = [];
44
45
  // Calculate weights based on distribution
45
46
  let weights = [];
@@ -75,6 +76,127 @@ function calculateLevels(midPrice, isBuy, totalSize, numLevels, rangePct, distri
75
76
  }
76
77
  return levels;
77
78
  }
79
+ export async function runScale(opts) {
80
+ const out = opts.output ?? ((line) => console.log(line));
81
+ const distribution = opts.distribution ?? 'linear';
82
+ const reduceOnly = opts.reduceOnly ?? false;
83
+ const tif = opts.tif ?? 'Gtc';
84
+ const rollbackOnPartial = opts.rollbackOnPartial ?? true;
85
+ const isBuy = opts.side === 'buy';
86
+ if (!opts.coin)
87
+ throw new Error('coin is required');
88
+ if (opts.side !== 'buy' && opts.side !== 'sell')
89
+ throw new Error('side must be buy or sell');
90
+ if (!Number.isFinite(opts.size) || opts.size <= 0)
91
+ throw new Error('size must be positive');
92
+ if (!Number.isInteger(opts.levels) || opts.levels <= 0)
93
+ throw new Error('levels must be a positive integer');
94
+ if (!Number.isFinite(opts.rangePct) || opts.rangePct <= 0)
95
+ throw new Error('rangePct must be positive');
96
+ if (!['linear', 'exponential', 'flat'].includes(distribution))
97
+ throw new Error('distribution must be linear, exponential, or flat');
98
+ const client = opts.client ?? getClient();
99
+ if (opts.verbose)
100
+ client.verbose = true;
101
+ out('Open Broker - Scale In/Out');
102
+ out('==========================\n');
103
+ const mids = await client.getAllMids();
104
+ const midPrice = parseFloat(mids[opts.coin]);
105
+ if (!midPrice)
106
+ throw new Error(`No market data for ${opts.coin}`);
107
+ const levels = calculateLevels(midPrice, isBuy, opts.size, opts.levels, opts.rangePct, distribution);
108
+ const notional = levels.reduce((sum, l) => sum + l.price * l.size, 0);
109
+ const avgPrice = notional / opts.size;
110
+ out('Order Plan');
111
+ out('----------');
112
+ out(`Coin: ${opts.coin}`);
113
+ out(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
114
+ out(`Total Size: ${opts.size}`);
115
+ out(`Current Mid: ${formatUsd(midPrice)}`);
116
+ out(`Levels: ${opts.levels}`);
117
+ out(`Range: ${opts.rangePct}% ${isBuy ? 'below' : 'above'} mid`);
118
+ out(`Distribution: ${distribution}`);
119
+ out(`Time in Force: ${tif}`);
120
+ out(`Reduce Only: ${reduceOnly ? 'Yes' : 'No'}`);
121
+ out(`Est. Notional: ${formatUsd(notional)}`);
122
+ out(`Avg Price: ${formatUsd(avgPrice)}`);
123
+ out('\nOrder Grid');
124
+ out('----------');
125
+ out('Level | Price | Size | Distance');
126
+ out('------|--------------|------------|----------');
127
+ for (const level of levels) {
128
+ out(` ${level.level.toString().padStart(2)} | ` +
129
+ `${formatUsd(level.price).padStart(12)} | ` +
130
+ `${level.size.toFixed(6).padStart(10)} | ` +
131
+ `${level.distanceFromMid.toFixed(2)}%`);
132
+ }
133
+ if (opts.dryRun) {
134
+ out('\n🔍 Dry run - orders not placed');
135
+ return { status: 'dry', levels, restingOids: [], filledOids: [], errors: [], rolledBack: false };
136
+ }
137
+ out('\nPlacing ladder as a bulk order...\n');
138
+ const response = await client.bulkOrder(levels.map((level) => ({
139
+ coin: opts.coin,
140
+ isBuy,
141
+ size: level.size,
142
+ price: level.price,
143
+ tif,
144
+ reduceOnly,
145
+ leverage: opts.leverage,
146
+ })));
147
+ const restingOids = [];
148
+ const filledOids = [];
149
+ const errors = [];
150
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
151
+ response.response.data.statuses.forEach((status, index) => {
152
+ const level = levels[index];
153
+ if (status?.resting) {
154
+ restingOids.push(status.resting.oid);
155
+ out(`Level ${level.level}: ✅ OID ${status.resting.oid}`);
156
+ }
157
+ else if (status?.filled) {
158
+ filledOids.push(status.filled.oid);
159
+ out(`Level ${level.level}: ✅ Filled ${status.filled.totalSz} @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
160
+ }
161
+ else if (status?.error) {
162
+ errors.push(`Level ${level.level}: ${status.error}`);
163
+ out(`Level ${level.level}: ❌ ${status.error}`);
164
+ }
165
+ else {
166
+ errors.push(`Level ${level.level}: Unknown status`);
167
+ out(`Level ${level.level}: ⚠️ Unknown status`);
168
+ }
169
+ });
170
+ }
171
+ else {
172
+ const reason = typeof response.response === 'string' ? response.response : 'Bulk order failed';
173
+ errors.push(reason);
174
+ out(`❌ ${reason}`);
175
+ }
176
+ let rolledBack = false;
177
+ if (errors.length > 0 && rollbackOnPartial && restingOids.length > 0) {
178
+ out('\nPartial ladder placement detected; cancelling resting ladder orders...');
179
+ await client.bulkCancel(restingOids.map((oid) => ({ coin: opts.coin, oid })));
180
+ rolledBack = true;
181
+ out(`Cancelled ${restingOids.length} resting order(s).`);
182
+ }
183
+ out('\n========== Summary ==========');
184
+ out(`Orders Placed: ${restingOids.length + filledOids.length}/${opts.levels}`);
185
+ if (errors.length > 0)
186
+ out(`Failed: ${errors.length}`);
187
+ if (restingOids.length > 0)
188
+ out(`Resting OIDs: ${restingOids.join(', ')}`);
189
+ if (filledOids.length > 0)
190
+ out(`Filled OIDs: ${filledOids.join(', ')}`);
191
+ if (rolledBack)
192
+ out('Rollback: Resting orders cancelled');
193
+ const status = errors.length === 0
194
+ ? 'complete'
195
+ : restingOids.length + filledOids.length > 0
196
+ ? 'partial'
197
+ : 'failed';
198
+ return { status, levels, restingOids, filledOids, errors, rolledBack };
199
+ }
78
200
  async function main() {
79
201
  const args = parseArgs(process.argv.slice(2));
80
202
  const coin = args.coin;
@@ -91,15 +213,6 @@ async function main() {
91
213
  printUsage();
92
214
  process.exit(1);
93
215
  }
94
- if (side !== 'buy' && side !== 'sell') {
95
- console.error('Error: --side must be "buy" or "sell"');
96
- process.exit(1);
97
- }
98
- if (!['linear', 'exponential', 'flat'].includes(distribution)) {
99
- console.error('Error: --distribution must be linear, exponential, or flat');
100
- process.exit(1);
101
- }
102
- // Map uppercase CLI input to Pascal case for SDK
103
216
  const tifMap = {
104
217
  'GTC': 'Gtc',
105
218
  'ALO': 'Alo'
@@ -109,104 +222,29 @@ async function main() {
109
222
  console.error('Error: --tif must be GTC or ALO');
110
223
  process.exit(1);
111
224
  }
112
- const isBuy = side === 'buy';
113
- const client = getClient();
114
- if (args.verbose) {
115
- client.verbose = true;
116
- }
117
- console.log('Open Broker - Scale In/Out');
118
- console.log('==========================\n');
119
225
  try {
120
- const mids = await client.getAllMids();
121
- const midPrice = parseFloat(mids[coin]);
122
- if (!midPrice) {
123
- console.error(`Error: No market data for ${coin}`);
226
+ const result = await runScale({
227
+ coin,
228
+ side: side,
229
+ size: totalSize,
230
+ levels: numLevels,
231
+ rangePct,
232
+ distribution,
233
+ leverage,
234
+ reduceOnly,
235
+ tif,
236
+ dryRun,
237
+ verbose: args.verbose,
238
+ });
239
+ if (result.status === 'failed')
124
240
  process.exit(1);
125
- }
126
- const levels = calculateLevels(midPrice, isBuy, totalSize, numLevels, rangePct, distribution);
127
- const notional = levels.reduce((sum, l) => sum + l.price * l.size, 0);
128
- const avgPrice = notional / totalSize;
129
- console.log('Order Plan');
130
- console.log('----------');
131
- console.log(`Coin: ${coin}`);
132
- console.log(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
133
- console.log(`Total Size: ${totalSize}`);
134
- console.log(`Current Mid: ${formatUsd(midPrice)}`);
135
- console.log(`Levels: ${numLevels}`);
136
- console.log(`Range: ${rangePct}% ${isBuy ? 'below' : 'above'} mid`);
137
- console.log(`Distribution: ${distribution}`);
138
- console.log(`Time in Force: ${tif}`);
139
- console.log(`Reduce Only: ${reduceOnly ? 'Yes' : 'No'}`);
140
- console.log(`Est. Notional: ${formatUsd(notional)}`);
141
- console.log(`Avg Price: ${formatUsd(avgPrice)}`);
142
- console.log('\nOrder Grid');
143
- console.log('----------');
144
- console.log('Level | Price | Size | Distance');
145
- console.log('------|--------------|------------|----------');
146
- for (const level of levels) {
147
- console.log(` ${level.level.toString().padStart(2)} | ` +
148
- `${formatUsd(level.price).padStart(12)} | ` +
149
- `${level.size.toFixed(6).padStart(10)} | ` +
150
- `${level.distanceFromMid.toFixed(2)}%`);
151
- }
152
- if (dryRun) {
153
- console.log('\n🔍 Dry run - orders not placed');
154
- return;
155
- }
156
- console.log('\nPlacing orders...\n');
157
- const results = [];
158
- for (const level of levels) {
159
- process.stdout.write(`Level ${level.level}: ${formatUsd(level.price)} x ${level.size.toFixed(6)}... `);
160
- try {
161
- const response = await client.limitOrder(coin, isBuy, level.size, level.price, tif, reduceOnly, leverage);
162
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
163
- const status = response.response.data.statuses[0];
164
- if (status?.resting) {
165
- console.log(`✅ OID: ${status.resting.oid}`);
166
- results.push({ level: level.level, oid: status.resting.oid });
167
- }
168
- else if (status?.filled) {
169
- console.log(`✅ Filled immediately @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
170
- results.push({ level: level.level, oid: status.filled.oid });
171
- }
172
- else if (status?.error) {
173
- console.log(`❌ ${status.error}`);
174
- results.push({ level: level.level, error: status.error });
175
- }
176
- else {
177
- console.log(`⚠️ Unknown status`);
178
- results.push({ level: level.level, error: 'Unknown status' });
179
- }
180
- }
181
- else {
182
- const error = typeof response.response === 'string' ? response.response : 'Failed';
183
- console.log(`❌ ${error}`);
184
- results.push({ level: level.level, error });
185
- }
186
- }
187
- catch (err) {
188
- const error = err instanceof Error ? err.message : String(err);
189
- console.log(`❌ ${error}`);
190
- results.push({ level: level.level, error });
191
- }
192
- // Small delay between orders
193
- await sleep(100);
194
- }
195
- // Summary
196
- const successful = results.filter(r => r.oid).length;
197
- const failed = results.filter(r => r.error).length;
198
- console.log('\n========== Summary ==========');
199
- console.log(`Orders Placed: ${successful}/${numLevels}`);
200
- if (failed > 0) {
201
- console.log(`Failed: ${failed}`);
202
- }
203
- if (successful > 0) {
204
- console.log(`Order IDs: ${results.filter(r => r.oid).map(r => r.oid).join(', ')}`);
205
- }
206
241
  }
207
242
  catch (error) {
208
- console.error('Error:', error);
243
+ console.error('Error:', error instanceof Error ? error.message : error);
209
244
  process.exit(1);
210
245
  }
211
246
  }
212
- main();
247
+ // Only run when invoked as a script — not when imported as a module.
248
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
249
+ main();
250
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.9.5",
3
+ "version": "1.9.6",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,10 +51,11 @@
51
51
  "funding-scan": "tsx scripts/info/funding-scan.ts",
52
52
  "outcomes": "tsx scripts/info/outcomes.ts",
53
53
  "build": "tsc",
54
- "prepublishOnly": "npm run build && npm run test:guardrails && npm run test:realtime && npm run test:install && npm run test:cli",
54
+ "prepublishOnly": "npm run build && npm run test:guardrails && npm run test:realtime && npm run test:advanced && npm run test:install && npm run test:cli",
55
55
  "test:cli": "node --import tsx bin/cli.ts --help",
56
56
  "test:guardrails": "node --import tsx --test scripts/auto/guardrails.test.ts",
57
57
  "test:realtime": "node --import tsx --test scripts/auto/realtime.test.ts",
58
+ "test:advanced": "node --import tsx --test scripts/operations/advanced-orders.test.ts",
58
59
  "test:install": "node --import tsx --test scripts/setup/package-catalog.test.ts"
59
60
  },
60
61
  "dependencies": {
@@ -2703,6 +2703,97 @@ export class HyperliquidClient {
2703
2703
  return this.triggerOrder(coin, isBuy, size, triggerPrice, triggerPrice, 'tp', true);
2704
2704
  }
2705
2705
 
2706
+ /**
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.
2710
+ */
2711
+ async tpslPair(
2712
+ coin: string,
2713
+ isBuy: boolean,
2714
+ size: number,
2715
+ takeProfitPrice: number,
2716
+ stopLossPrice: number,
2717
+ stopLossSlippageBps: number = 100,
2718
+ leverage?: number
2719
+ ): Promise<OrderResponse> {
2720
+ await this.requireTrading();
2721
+ await this.getMetaAndAssetCtxs();
2722
+
2723
+ if (leverage && !this.isHip3(coin)) {
2724
+ this.log(`Setting leverage for ${coin} to ${leverage}x cross`);
2725
+ await this.updateLeverage(coin, leverage, true);
2726
+ }
2727
+
2728
+ const slippageMult = stopLossSlippageBps / 10000;
2729
+ const stopLossLimitPrice = isBuy
2730
+ ? stopLossPrice * (1 + slippageMult)
2731
+ : stopLossPrice * (1 - slippageMult);
2732
+
2733
+ await this.ensureHip3Ready(coin, size * Math.max(takeProfitPrice, stopLossLimitPrice), leverage);
2734
+
2735
+ const assetIndex = this.getAssetIndex(coin);
2736
+ const szDecimals = this.getSzDecimals(coin);
2737
+ const roundedSize = roundSize(size, szDecimals);
2738
+
2739
+ const orderWires = [
2740
+ {
2741
+ a: assetIndex,
2742
+ b: isBuy,
2743
+ p: roundPrice(takeProfitPrice, szDecimals),
2744
+ s: roundedSize,
2745
+ r: true,
2746
+ t: {
2747
+ trigger: {
2748
+ triggerPx: roundPrice(takeProfitPrice, szDecimals),
2749
+ isMarket: false,
2750
+ tpsl: 'tp' as const,
2751
+ },
2752
+ },
2753
+ },
2754
+ {
2755
+ a: assetIndex,
2756
+ b: isBuy,
2757
+ p: roundPrice(stopLossLimitPrice, szDecimals),
2758
+ s: roundedSize,
2759
+ r: true,
2760
+ t: {
2761
+ trigger: {
2762
+ triggerPx: roundPrice(stopLossPrice, szDecimals),
2763
+ isMarket: false,
2764
+ tpsl: 'sl' as const,
2765
+ },
2766
+ },
2767
+ },
2768
+ ];
2769
+
2770
+ const orderRequest: {
2771
+ orders: typeof orderWires;
2772
+ grouping: 'normalTpsl';
2773
+ builder?: BuilderInfo;
2774
+ } = {
2775
+ orders: orderWires,
2776
+ grouping: 'normalTpsl',
2777
+ };
2778
+
2779
+ if (!this.isTestnet && this.config.builderAddress !== '0x0000000000000000000000000000000000000000') {
2780
+ orderRequest.builder = this.builderInfo;
2781
+ this.log('Including builder fee:', this.builderInfo);
2782
+ }
2783
+
2784
+ try {
2785
+ const response = await this.exchange.order(orderRequest, this.vaultParam);
2786
+ this.log('TP/SL pair response:', JSON.stringify(response, null, 2));
2787
+ return response as unknown as OrderResponse;
2788
+ } catch (error) {
2789
+ this.log('TP/SL pair error:', error);
2790
+ return {
2791
+ status: 'err',
2792
+ response: error instanceof Error ? error.message : String(error),
2793
+ };
2794
+ }
2795
+ }
2796
+
2706
2797
  async cancel(coin: string, oid: number): Promise<CancelResponse> {
2707
2798
  await this.requireTrading();
2708
2799
  await this.getMetaAndAssetCtxs();
package/scripts/lib.ts CHANGED
@@ -59,6 +59,9 @@ export type { BracketOptions, BracketResult } from './operations/bracket.js';
59
59
  export { runChase } from './operations/chase.js';
60
60
  export type { ChaseOptions, ChaseResult } from './operations/chase.js';
61
61
 
62
+ export { runScale, calculateLevels } from './operations/scale.js';
63
+ export type { ScaleOptions, ScaleResult, OrderLevel } from './operations/scale.js';
64
+
62
65
  // ── Automation runtime ──────────────────────────────────────────────
63
66
 
64
67
  export {