openbroker 1.9.6 → 1.10.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.
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env npx tsx
2
2
  // Set Take Profit and/or Stop Loss on an existing position
3
3
  import { getClient } from '../core/client.js';
4
- import { formatUsd, parseArgs, sleep } from '../core/utils.js';
4
+ import { formatUsd, parseArgs, parseOrderStatus } from '../core/utils.js';
5
5
  function printUsage() {
6
6
  console.log(`
7
7
  Open Broker - Set TP/SL
8
8
  =======================
9
9
 
10
10
  Add take profit and/or stop loss orders to an existing position.
11
- Uses trigger orders that execute when price reaches the target.
11
+ Placed as one batch with Hyperliquid's positionTpsl grouping: the triggers
12
+ track the open position and OCO-cancel each other when one fires.
12
13
 
13
14
  Usage:
14
15
  npx tsx scripts/operations/set-tpsl.ts --coin <COIN> [--tp <PRICE>] [--sl <PRICE>]
@@ -18,7 +19,10 @@ Options:
18
19
  --tp Take profit trigger price
19
20
  --sl Stop loss trigger price
20
21
  --size Size to protect (default: full position size)
21
- --sl-slippage Stop loss slippage in bps (default: 100 = 1%)
22
+ --sl-slippage Stop loss fill cap past the trigger in bps (default: 100 = 1%)
23
+ --sl-limit Place the SL as a stop-limit instead of a market trigger.
24
+ Warning: a gap move past the limit band can skip the stop
25
+ entirely and leave the position unprotected.
22
26
  --dry Dry run - show orders without placing
23
27
  --verbose Show debug output
24
28
 
@@ -44,9 +48,11 @@ Examples:
44
48
  How Trigger Orders Work:
45
49
  - TP/SL are trigger orders, NOT regular limit orders
46
50
  - They sit dormant until price reaches the trigger level
47
- - Once triggered, they execute as limit orders
51
+ - TP executes as a limit order at the target; SL executes as a market
52
+ order capped by the slippage band (use --sl-limit for a stop-limit)
48
53
  - These are reduce-only orders (close position, don't reverse)
49
- - SL has slippage buffer to ensure fill in fast markets
54
+ - positionTpsl grouping ties them to the position: when one fires and
55
+ closes the position, the venue cancels the other automatically
50
56
  `);
51
57
  }
52
58
  function parsePrice(input, entryPrice, isLong) {
@@ -86,6 +92,7 @@ async function main() {
86
92
  const slInput = args.sl;
87
93
  const sizeOverride = args.size ? parseFloat(args.size) : undefined;
88
94
  const slSlippage = args['sl-slippage'] ? parseInt(args['sl-slippage']) : 100;
95
+ const slMarket = !args['sl-limit'];
89
96
  const dryRun = args.dry;
90
97
  if (!coin) {
91
98
  printUsage();
@@ -137,27 +144,31 @@ async function main() {
137
144
  console.log('Use absolute price (e.g., 35), percentage (e.g., -5%), or "entry"');
138
145
  process.exit(1);
139
146
  }
140
- // Validate TP/SL make sense for position direction
147
+ // Triggers must sit on the correct side of the LIVE price, or the
148
+ // exchange fires them immediately on placement.
149
+ const directionErrors = [];
141
150
  if (isLong) {
142
151
  if (tpPrice && tpPrice <= currentPrice) {
143
- console.warn(`⚠️ Warning: TP (${formatUsd(tpPrice)}) is at or below current price (${formatUsd(currentPrice)})`);
144
- console.warn(' For LONG positions, TP should be above current price');
152
+ directionErrors.push(`TP (${formatUsd(tpPrice)}) must be above the current price (${formatUsd(currentPrice)}) for a LONG`);
145
153
  }
146
154
  if (slPrice && slPrice >= currentPrice) {
147
- console.warn(`⚠️ Warning: SL (${formatUsd(slPrice)}) is at or above current price (${formatUsd(currentPrice)})`);
148
- console.warn(' For LONG positions, SL should be below current price');
155
+ directionErrors.push(`SL (${formatUsd(slPrice)}) must be below the current price (${formatUsd(currentPrice)}) for a LONG`);
149
156
  }
150
157
  }
151
158
  else {
152
159
  if (tpPrice && tpPrice >= currentPrice) {
153
- console.warn(`⚠️ Warning: TP (${formatUsd(tpPrice)}) is at or above current price (${formatUsd(currentPrice)})`);
154
- console.warn(' For SHORT positions, TP should be below current price');
160
+ directionErrors.push(`TP (${formatUsd(tpPrice)}) must be below the current price (${formatUsd(currentPrice)}) for a SHORT`);
155
161
  }
156
162
  if (slPrice && slPrice <= currentPrice) {
157
- console.warn(`⚠️ Warning: SL (${formatUsd(slPrice)}) is at or below current price (${formatUsd(currentPrice)})`);
158
- console.warn(' For SHORT positions, SL should be above current price');
163
+ directionErrors.push(`SL (${formatUsd(slPrice)}) must be above the current price (${formatUsd(currentPrice)}) for a SHORT`);
159
164
  }
160
165
  }
166
+ if (directionErrors.length > 0) {
167
+ for (const err of directionErrors)
168
+ console.error(`Error: ${err}`);
169
+ console.error('A trigger on the wrong side of the live price fires immediately on placement.');
170
+ process.exit(1);
171
+ }
161
172
  // Calculate risk/reward
162
173
  let tpDistance = 0, slDistance = 0, riskReward = 0;
163
174
  if (tpPrice) {
@@ -192,7 +203,7 @@ async function main() {
192
203
  const slLimitPrice = isLong
193
204
  ? slPrice * (1 - slSlippage / 10000)
194
205
  : slPrice * (1 + slSlippage / 10000);
195
- console.log(`Stop Loss: ${slSide} ${size} @ ${formatUsd(slPrice)} trigger, ${formatUsd(slLimitPrice)} limit (-${slDistance.toFixed(2)}%)`);
206
+ console.log(`Stop Loss: ${slSide} ${size} @ ${formatUsd(slPrice)} trigger, ${slMarket ? `market (fill capped at ${formatUsd(slLimitPrice)})` : `${formatUsd(slLimitPrice)} limit`} (-${slDistance.toFixed(2)}%)`);
196
207
  }
197
208
  if (riskReward > 0) {
198
209
  console.log(`Risk/Reward: 1:${riskReward.toFixed(2)}`);
@@ -210,51 +221,51 @@ async function main() {
210
221
  console.log('\n🔍 Dry run - orders not placed');
211
222
  return;
212
223
  }
213
- console.log('\nPlacing trigger orders...\n');
214
- // Place Take Profit
224
+ console.log('\nPlacing trigger orders (positionTpsl batch)...\n');
225
+ // One batch with positionTpsl grouping: the venue ties the triggers to
226
+ // the open position and OCO-cancels the survivor when one fires.
227
+ const exitSide = !isLong; // Opposite of position direction
228
+ const response = await client.tpslOrders(coin, exitSide, size, {
229
+ takeProfitPrice: tpPrice ?? undefined,
230
+ stopLossPrice: slPrice ?? undefined,
231
+ stopLossSlippageBps: slSlippage,
232
+ stopLossIsMarket: slMarket,
233
+ grouping: 'positionTpsl',
234
+ });
215
235
  let tpOid = null;
216
- if (tpPrice) {
217
- const tpSide = !isLong; // Opposite of position direction
218
- const response = await client.takeProfit(coin, tpSide, size, tpPrice);
219
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
220
- const status = response.response.data.statuses[0];
221
- if (status?.resting) {
222
- tpOid = status.resting.oid;
223
- console.log(`✅ Take Profit placed @ ${formatUsd(tpPrice)} (OID: ${tpOid})`);
224
- }
225
- else if (status?.error) {
226
- console.log(`❌ TP failed: ${status.error}`);
227
- }
228
- else {
229
- console.log(`⚠️ TP status:`, JSON.stringify(status));
230
- }
231
- }
232
- else {
233
- console.log(`❌ TP failed: ${typeof response.response === 'string' ? response.response : 'Unknown error'}`);
234
- }
235
- await sleep(200);
236
- }
237
- // Place Stop Loss
238
236
  let slOid = null;
239
- if (slPrice) {
240
- const slSide = !isLong; // Opposite of position direction
241
- const response = await client.stopLoss(coin, slSide, size, slPrice, slSlippage);
242
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
243
- const status = response.response.data.statuses[0];
244
- if (status?.resting) {
245
- slOid = status.resting.oid;
246
- console.log(`✅ Stop Loss placed @ ${formatUsd(slPrice)} (OID: ${slOid})`);
237
+ let placementErrors = 0;
238
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
239
+ const statuses = response.response.data.statuses.map(parseOrderStatus);
240
+ let idx = 0;
241
+ for (const leg of [tpPrice ? 'Take Profit' : null, slPrice ? 'Stop Loss' : null]) {
242
+ if (!leg)
243
+ continue;
244
+ const status = statuses[idx++];
245
+ const price = leg === 'Take Profit' ? tpPrice : slPrice;
246
+ if (status?.kind === 'resting') {
247
+ if (leg === 'Take Profit')
248
+ tpOid = status.oid;
249
+ else
250
+ slOid = status.oid;
251
+ console.log(`✅ ${leg} placed @ ${formatUsd(price)} (OID: ${status.oid})`);
252
+ }
253
+ else if (status?.kind === 'waiting') {
254
+ console.log(`✅ ${leg} armed @ ${formatUsd(price)} (${status.state})`);
247
255
  }
248
- else if (status?.error) {
249
- console.log(`❌ SL failed: ${status.error}`);
256
+ else if (status?.kind === 'error') {
257
+ placementErrors++;
258
+ console.log(`❌ ${leg} failed: ${status.error}`);
250
259
  }
251
260
  else {
252
- console.log(`⚠️ SL status:`, JSON.stringify(status));
261
+ placementErrors++;
262
+ console.log(`⚠️ ${leg} status:`, JSON.stringify(status));
253
263
  }
254
264
  }
255
- else {
256
- console.log(`❌ SL failed: ${typeof response.response === 'string' ? response.response : 'Unknown error'}`);
257
- }
265
+ }
266
+ else {
267
+ placementErrors++;
268
+ console.log(`❌ TP/SL failed: ${typeof response.response === 'string' ? response.response : 'Unknown error'}`);
258
269
  }
259
270
  // Summary
260
271
  console.log('\n========== Summary ==========');
@@ -263,11 +274,12 @@ async function main() {
263
274
  if (tpOid)
264
275
  console.log(`Take Profit: ${formatUsd(tpPrice)} (OID: ${tpOid})`);
265
276
  if (slOid)
266
- console.log(`Stop Loss: ${formatUsd(slPrice)} (OID: ${slOid})`);
267
- if (tpOid && slOid) {
268
- console.log(`\n💡 Tip: When one order fills, cancel the other manually:`);
269
- console.log(` npx tsx scripts/operations/cancel.ts --coin ${coin} --oid <OID>`);
277
+ console.log(`Stop Loss: ${formatUsd(slPrice)} (OID: ${slOid}, ${slMarket ? 'market trigger' : 'stop-limit'})`);
278
+ if (placementErrors === 0 && tpPrice && slPrice) {
279
+ console.log(`\n💡 The triggers track the position: when one fires, the venue cancels the other.`);
270
280
  }
281
+ if (placementErrors > 0)
282
+ process.exit(1);
271
283
  }
272
284
  catch (error) {
273
285
  console.error('Error:', error);
@@ -21,15 +21,18 @@ Options:
21
21
  --type Order type: tp (take profit) or sl (stop loss)
22
22
  --limit Limit price when triggered (default: trigger price for TP, with slippage for SL)
23
23
  --slippage Slippage for SL in bps (default: 100 = 1%)
24
+ --exec Execution when triggered: market or limit
25
+ (default: market for SL, limit for TP)
24
26
  --leverage Set leverage (e.g., 10 for 10x). Cross for main perps, isolated for HIP-3
25
27
  --reduce Reduce-only order (default: true for TP/SL)
26
28
  --dry Dry run - show order without placing
27
29
 
28
30
  Trigger Order Behavior:
29
31
  - Order is dormant until price reaches trigger level
30
- - Once triggered, becomes a limit order at the limit price
31
- - TP: Limit price = trigger price (favorable)
32
- - SL: Limit price = trigger ± slippage (ensures fill)
32
+ - TP: rests as a limit order at the limit price (favorable)
33
+ - SL: fires as a market order capped by the limit price (trigger ± slippage);
34
+ pass --exec limit for a stop-limit, but note a gap move past the band can
35
+ skip the stop entirely and leave the position unprotected
33
36
 
34
37
  Examples:
35
38
  # Take profit: sell 0.5 HYPE when price rises to $40
@@ -58,6 +61,11 @@ async function main() {
58
61
  const orderType = args.type;
59
62
  const limitPriceOverride = args.limit ? parseFloat(args.limit) : undefined;
60
63
  const slippageBps = args.slippage ? parseInt(args.slippage) : 100;
64
+ const execOverride = args.exec;
65
+ if (execOverride && execOverride !== 'market' && execOverride !== 'limit') {
66
+ console.error('Error: --exec must be "market" or "limit"');
67
+ process.exit(1);
68
+ }
61
69
  const leverage = args.leverage ? parseInt(args.leverage) : undefined;
62
70
  const reduceOnly = args.reduce !== 'false'; // Default true
63
71
  const dryRun = args.dry;
@@ -103,12 +111,16 @@ async function main() {
103
111
  limitPrice = triggerPrice;
104
112
  }
105
113
  else {
106
- // SL: add slippage to ensure fill
114
+ // SL: slippage band past the trigger — caps the market fill, or is the
115
+ // resting price for a stop-limit
107
116
  const slippageMult = slippageBps / 10000;
108
117
  limitPrice = isBuy
109
118
  ? triggerPrice * (1 + slippageMult)
110
119
  : triggerPrice * (1 - slippageMult);
111
120
  }
121
+ // SL fires as a market trigger by default (a stop-limit can be gapped
122
+ // over and never fill); TP rests as a limit at the target.
123
+ const isMarket = execOverride ? execOverride === 'market' : tpsl === 'sl';
112
124
  const distanceFromCurrent = ((triggerPrice - currentPrice) / currentPrice) * 100;
113
125
  const notional = triggerPrice * size;
114
126
  console.log('Trigger Order Details');
@@ -119,7 +131,7 @@ async function main() {
119
131
  console.log(`Size: ${size}`);
120
132
  console.log(`Current Price: ${formatUsd(currentPrice)}`);
121
133
  console.log(`Trigger Price: ${formatUsd(triggerPrice)} (${distanceFromCurrent >= 0 ? '+' : ''}${distanceFromCurrent.toFixed(2)}%)`);
122
- console.log(`Limit Price: ${formatUsd(limitPrice)}`);
134
+ console.log(`Execution: ${isMarket ? `market (fill capped at ${formatUsd(limitPrice)})` : `limit @ ${formatUsd(limitPrice)}`}`);
123
135
  console.log(`Reduce Only: ${reduceOnly ? 'Yes' : 'No'}`);
124
136
  console.log(`Est. Notional: ${formatUsd(notional)}`);
125
137
  // Sanity checks
@@ -144,7 +156,7 @@ async function main() {
144
156
  return;
145
157
  }
146
158
  console.log('\nPlacing trigger order...');
147
- const response = await client.triggerOrder(coin, isBuy, size, triggerPrice, limitPrice, tpsl, reduceOnly, leverage);
159
+ const response = await client.triggerOrder(coin, isBuy, size, triggerPrice, limitPrice, tpsl, reduceOnly, leverage, isMarket);
148
160
  console.log('\nResult');
149
161
  console.log('------');
150
162
  if (response.status === 'ok' && response.response && typeof response.response === 'object') {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env npx tsx
2
2
  // TWAP (Time-Weighted Average Price) execution using Hyperliquid's native TWAP orders
3
3
  import { getClient } from '../core/client.js';
4
- import { formatUsd, parseArgs } from '../core/utils.js';
4
+ import { MIN_ORDER_NOTIONAL_USD, formatUsd, parseArgs } from '../core/utils.js';
5
5
  function printUsage() {
6
6
  console.log(`
7
7
  Open Broker - TWAP Order (Native)
@@ -77,6 +77,10 @@ async function main() {
77
77
  process.exit(1);
78
78
  }
79
79
  const notional = midPrice * totalSize;
80
+ // Native TWAP fires a sub-order every 30s; each must clear the exchange minimum.
81
+ const sliceCount = durationMinutes * 2;
82
+ const perSliceNotional = notional / sliceCount;
83
+ const sliceBelowMinimum = !reduceOnly && perSliceNotional < MIN_ORDER_NOTIONAL_USD;
80
84
  console.log('Order Details');
81
85
  console.log('-------------');
82
86
  console.log(`Coin: ${coin}`);
@@ -85,16 +89,24 @@ async function main() {
85
89
  console.log(`Current Price: ${formatUsd(midPrice)}`);
86
90
  console.log(`Est. Notional: ${formatUsd(notional)}`);
87
91
  console.log(`Duration: ${formatDuration(durationMinutes * 60)}`);
92
+ console.log(`Slices: ~${sliceCount} (every 30s, ~${formatUsd(perSliceNotional)} each)`);
88
93
  console.log(`Randomize: ${randomize ? 'yes' : 'no'}`);
89
94
  console.log(`Reduce Only: ${reduceOnly ? 'yes' : 'no'}`);
90
95
  if (leverage) {
91
96
  console.log(`Leverage: ${leverage}x`);
92
97
  }
93
98
  if (dryRun) {
99
+ if (sliceBelowMinimum) {
100
+ console.log(`\n⚠️ TWAP slices of ~${formatUsd(perSliceNotional)} fall below the $${MIN_ORDER_NOTIONAL_USD} exchange minimum — shorten the duration or increase the size.`);
101
+ }
94
102
  console.log('\nDry run - no order placed.');
95
103
  console.log('The exchange will handle order slicing and timing automatically.');
96
104
  return;
97
105
  }
106
+ if (sliceBelowMinimum) {
107
+ console.error(`Error: TWAP slices of ~${formatUsd(perSliceNotional)} fall below the $${MIN_ORDER_NOTIONAL_USD} exchange minimum — shorten the duration or increase the size.`);
108
+ process.exit(1);
109
+ }
98
110
  console.log('\nPlacing native TWAP order...\n');
99
111
  const response = await client.twapOrder(coin, isBuy, totalSize, durationMinutes, randomize, reduceOnly, leverage);
100
112
  // SDK's TwapOrderSuccessResponse excludes the error variant — the SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.9.6",
3
+ "version": "1.10.0",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {