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.
- package/CHANGELOG.md +19 -0
- package/SKILL.md +13 -4
- package/dist/core/client.d.ts +60 -6
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +190 -26
- 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/lib.d.ts +2 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -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/chase.d.ts +1 -1
- package/dist/operations/chase.d.ts.map +1 -1
- package/dist/operations/chase.js +58 -23
- 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 +1 -1
- package/scripts/core/client.ts +249 -30
- package/scripts/core/utils.ts +37 -0
- package/scripts/lib.ts +3 -0
- package/scripts/operations/advanced-orders.test.ts +347 -14
- package/scripts/operations/bracket.ts +252 -73
- package/scripts/operations/chase.ts +57 -24
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Set Take Profit and/or Stop Loss on an existing position
|
|
3
3
|
|
|
4
4
|
import { getClient } from '../core/client.js';
|
|
5
|
-
import { formatUsd, parseArgs,
|
|
5
|
+
import { formatUsd, parseArgs, parseOrderStatus } from '../core/utils.js';
|
|
6
6
|
|
|
7
7
|
function printUsage() {
|
|
8
8
|
console.log(`
|
|
@@ -10,7 +10,8 @@ Open Broker - Set TP/SL
|
|
|
10
10
|
=======================
|
|
11
11
|
|
|
12
12
|
Add take profit and/or stop loss orders to an existing position.
|
|
13
|
-
|
|
13
|
+
Placed as one batch with Hyperliquid's positionTpsl grouping: the triggers
|
|
14
|
+
track the open position and OCO-cancel each other when one fires.
|
|
14
15
|
|
|
15
16
|
Usage:
|
|
16
17
|
npx tsx scripts/operations/set-tpsl.ts --coin <COIN> [--tp <PRICE>] [--sl <PRICE>]
|
|
@@ -20,7 +21,10 @@ Options:
|
|
|
20
21
|
--tp Take profit trigger price
|
|
21
22
|
--sl Stop loss trigger price
|
|
22
23
|
--size Size to protect (default: full position size)
|
|
23
|
-
--sl-slippage Stop loss
|
|
24
|
+
--sl-slippage Stop loss fill cap past the trigger in bps (default: 100 = 1%)
|
|
25
|
+
--sl-limit Place the SL as a stop-limit instead of a market trigger.
|
|
26
|
+
Warning: a gap move past the limit band can skip the stop
|
|
27
|
+
entirely and leave the position unprotected.
|
|
24
28
|
--dry Dry run - show orders without placing
|
|
25
29
|
--verbose Show debug output
|
|
26
30
|
|
|
@@ -46,9 +50,11 @@ Examples:
|
|
|
46
50
|
How Trigger Orders Work:
|
|
47
51
|
- TP/SL are trigger orders, NOT regular limit orders
|
|
48
52
|
- They sit dormant until price reaches the trigger level
|
|
49
|
-
-
|
|
53
|
+
- TP executes as a limit order at the target; SL executes as a market
|
|
54
|
+
order capped by the slippage band (use --sl-limit for a stop-limit)
|
|
50
55
|
- These are reduce-only orders (close position, don't reverse)
|
|
51
|
-
-
|
|
56
|
+
- positionTpsl grouping ties them to the position: when one fires and
|
|
57
|
+
closes the position, the venue cancels the other automatically
|
|
52
58
|
`);
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -95,6 +101,7 @@ async function main() {
|
|
|
95
101
|
const slInput = args.sl as string | undefined;
|
|
96
102
|
const sizeOverride = args.size ? parseFloat(args.size as string) : undefined;
|
|
97
103
|
const slSlippage = args['sl-slippage'] ? parseInt(args['sl-slippage'] as string) : 100;
|
|
104
|
+
const slMarket = !(args['sl-limit'] as boolean);
|
|
98
105
|
const dryRun = args.dry as boolean;
|
|
99
106
|
|
|
100
107
|
if (!coin) {
|
|
@@ -159,26 +166,29 @@ async function main() {
|
|
|
159
166
|
process.exit(1);
|
|
160
167
|
}
|
|
161
168
|
|
|
162
|
-
//
|
|
169
|
+
// Triggers must sit on the correct side of the LIVE price, or the
|
|
170
|
+
// exchange fires them immediately on placement.
|
|
171
|
+
const directionErrors: string[] = [];
|
|
163
172
|
if (isLong) {
|
|
164
173
|
if (tpPrice && tpPrice <= currentPrice) {
|
|
165
|
-
|
|
166
|
-
console.warn(' For LONG positions, TP should be above current price');
|
|
174
|
+
directionErrors.push(`TP (${formatUsd(tpPrice)}) must be above the current price (${formatUsd(currentPrice)}) for a LONG`);
|
|
167
175
|
}
|
|
168
176
|
if (slPrice && slPrice >= currentPrice) {
|
|
169
|
-
|
|
170
|
-
console.warn(' For LONG positions, SL should be below current price');
|
|
177
|
+
directionErrors.push(`SL (${formatUsd(slPrice)}) must be below the current price (${formatUsd(currentPrice)}) for a LONG`);
|
|
171
178
|
}
|
|
172
179
|
} else {
|
|
173
180
|
if (tpPrice && tpPrice >= currentPrice) {
|
|
174
|
-
|
|
175
|
-
console.warn(' For SHORT positions, TP should be below current price');
|
|
181
|
+
directionErrors.push(`TP (${formatUsd(tpPrice)}) must be below the current price (${formatUsd(currentPrice)}) for a SHORT`);
|
|
176
182
|
}
|
|
177
183
|
if (slPrice && slPrice <= currentPrice) {
|
|
178
|
-
|
|
179
|
-
console.warn(' For SHORT positions, SL should be above current price');
|
|
184
|
+
directionErrors.push(`SL (${formatUsd(slPrice)}) must be above the current price (${formatUsd(currentPrice)}) for a SHORT`);
|
|
180
185
|
}
|
|
181
186
|
}
|
|
187
|
+
if (directionErrors.length > 0) {
|
|
188
|
+
for (const err of directionErrors) console.error(`Error: ${err}`);
|
|
189
|
+
console.error('A trigger on the wrong side of the live price fires immediately on placement.');
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
182
192
|
|
|
183
193
|
// Calculate risk/reward
|
|
184
194
|
let tpDistance = 0, slDistance = 0, riskReward = 0;
|
|
@@ -216,7 +226,7 @@ async function main() {
|
|
|
216
226
|
const slLimitPrice = isLong
|
|
217
227
|
? slPrice * (1 - slSlippage / 10000)
|
|
218
228
|
: slPrice * (1 + slSlippage / 10000);
|
|
219
|
-
console.log(`Stop Loss: ${slSide} ${size} @ ${formatUsd(slPrice)} trigger, ${formatUsd(slLimitPrice)} limit (-${slDistance.toFixed(2)}%)`);
|
|
229
|
+
console.log(`Stop Loss: ${slSide} ${size} @ ${formatUsd(slPrice)} trigger, ${slMarket ? `market (fill capped at ${formatUsd(slLimitPrice)})` : `${formatUsd(slLimitPrice)} limit`} (-${slDistance.toFixed(2)}%)`);
|
|
220
230
|
}
|
|
221
231
|
if (riskReward > 0) {
|
|
222
232
|
console.log(`Risk/Reward: 1:${riskReward.toFixed(2)}`);
|
|
@@ -235,50 +245,45 @@ async function main() {
|
|
|
235
245
|
return;
|
|
236
246
|
}
|
|
237
247
|
|
|
238
|
-
console.log('\nPlacing trigger orders...\n');
|
|
239
|
-
|
|
240
|
-
// Place Take Profit
|
|
241
|
-
let tpOid: number | null = null;
|
|
242
|
-
if (tpPrice) {
|
|
243
|
-
const tpSide = !isLong; // Opposite of position direction
|
|
244
|
-
const response = await client.takeProfit(coin, tpSide, size, tpPrice);
|
|
245
|
-
|
|
246
|
-
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
247
|
-
const status = response.response.data.statuses[0];
|
|
248
|
-
if (status?.resting) {
|
|
249
|
-
tpOid = status.resting.oid;
|
|
250
|
-
console.log(`✅ Take Profit placed @ ${formatUsd(tpPrice)} (OID: ${tpOid})`);
|
|
251
|
-
} else if (status?.error) {
|
|
252
|
-
console.log(`❌ TP failed: ${status.error}`);
|
|
253
|
-
} else {
|
|
254
|
-
console.log(`⚠️ TP status:`, JSON.stringify(status));
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
console.log(`❌ TP failed: ${typeof response.response === 'string' ? response.response : 'Unknown error'}`);
|
|
258
|
-
}
|
|
248
|
+
console.log('\nPlacing trigger orders (positionTpsl batch)...\n');
|
|
259
249
|
|
|
260
|
-
|
|
261
|
-
|
|
250
|
+
// One batch with positionTpsl grouping: the venue ties the triggers to
|
|
251
|
+
// the open position and OCO-cancels the survivor when one fires.
|
|
252
|
+
const exitSide = !isLong; // Opposite of position direction
|
|
253
|
+
const response = await client.tpslOrders(coin, exitSide, size, {
|
|
254
|
+
takeProfitPrice: tpPrice ?? undefined,
|
|
255
|
+
stopLossPrice: slPrice ?? undefined,
|
|
256
|
+
stopLossSlippageBps: slSlippage,
|
|
257
|
+
stopLossIsMarket: slMarket,
|
|
258
|
+
grouping: 'positionTpsl',
|
|
259
|
+
});
|
|
262
260
|
|
|
263
|
-
|
|
261
|
+
let tpOid: number | null = null;
|
|
264
262
|
let slOid: number | null = null;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
console.log(
|
|
263
|
+
let placementErrors = 0;
|
|
264
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
265
|
+
const statuses = response.response.data.statuses.map(parseOrderStatus);
|
|
266
|
+
let idx = 0;
|
|
267
|
+
for (const leg of [tpPrice ? 'Take Profit' : null, slPrice ? 'Stop Loss' : null]) {
|
|
268
|
+
if (!leg) continue;
|
|
269
|
+
const status = statuses[idx++];
|
|
270
|
+
const price = leg === 'Take Profit' ? tpPrice! : slPrice!;
|
|
271
|
+
if (status?.kind === 'resting') {
|
|
272
|
+
if (leg === 'Take Profit') tpOid = status.oid; else slOid = status.oid;
|
|
273
|
+
console.log(`✅ ${leg} placed @ ${formatUsd(price)} (OID: ${status.oid})`);
|
|
274
|
+
} else if (status?.kind === 'waiting') {
|
|
275
|
+
console.log(`✅ ${leg} armed @ ${formatUsd(price)} (${status.state})`);
|
|
276
|
+
} else if (status?.kind === 'error') {
|
|
277
|
+
placementErrors++;
|
|
278
|
+
console.log(`❌ ${leg} failed: ${status.error}`);
|
|
276
279
|
} else {
|
|
277
|
-
|
|
280
|
+
placementErrors++;
|
|
281
|
+
console.log(`⚠️ ${leg} status:`, JSON.stringify(status));
|
|
278
282
|
}
|
|
279
|
-
} else {
|
|
280
|
-
console.log(`❌ SL failed: ${typeof response.response === 'string' ? response.response : 'Unknown error'}`);
|
|
281
283
|
}
|
|
284
|
+
} else {
|
|
285
|
+
placementErrors++;
|
|
286
|
+
console.log(`❌ TP/SL failed: ${typeof response.response === 'string' ? response.response : 'Unknown error'}`);
|
|
282
287
|
}
|
|
283
288
|
|
|
284
289
|
// Summary
|
|
@@ -286,12 +291,12 @@ async function main() {
|
|
|
286
291
|
console.log(`Position: ${isLong ? 'LONG' : 'SHORT'} ${absSize} ${coin}`);
|
|
287
292
|
console.log(`Entry: ${formatUsd(entryPrice)}`);
|
|
288
293
|
if (tpOid) console.log(`Take Profit: ${formatUsd(tpPrice!)} (OID: ${tpOid})`);
|
|
289
|
-
if (slOid) console.log(`Stop Loss: ${formatUsd(slPrice!)} (OID: ${slOid})`);
|
|
294
|
+
if (slOid) console.log(`Stop Loss: ${formatUsd(slPrice!)} (OID: ${slOid}, ${slMarket ? 'market trigger' : 'stop-limit'})`);
|
|
290
295
|
|
|
291
|
-
if (
|
|
292
|
-
console.log(`\n💡
|
|
293
|
-
console.log(` npx tsx scripts/operations/cancel.ts --coin ${coin} --oid <OID>`);
|
|
296
|
+
if (placementErrors === 0 && tpPrice && slPrice) {
|
|
297
|
+
console.log(`\n💡 The triggers track the position: when one fires, the venue cancels the other.`);
|
|
294
298
|
}
|
|
299
|
+
if (placementErrors > 0) process.exit(1);
|
|
295
300
|
|
|
296
301
|
} catch (error) {
|
|
297
302
|
console.error('Error:', error);
|
|
@@ -23,15 +23,18 @@ Options:
|
|
|
23
23
|
--type Order type: tp (take profit) or sl (stop loss)
|
|
24
24
|
--limit Limit price when triggered (default: trigger price for TP, with slippage for SL)
|
|
25
25
|
--slippage Slippage for SL in bps (default: 100 = 1%)
|
|
26
|
+
--exec Execution when triggered: market or limit
|
|
27
|
+
(default: market for SL, limit for TP)
|
|
26
28
|
--leverage Set leverage (e.g., 10 for 10x). Cross for main perps, isolated for HIP-3
|
|
27
29
|
--reduce Reduce-only order (default: true for TP/SL)
|
|
28
30
|
--dry Dry run - show order without placing
|
|
29
31
|
|
|
30
32
|
Trigger Order Behavior:
|
|
31
33
|
- Order is dormant until price reaches trigger level
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
|
|
34
|
+
- TP: rests as a limit order at the limit price (favorable)
|
|
35
|
+
- SL: fires as a market order capped by the limit price (trigger ± slippage);
|
|
36
|
+
pass --exec limit for a stop-limit, but note a gap move past the band can
|
|
37
|
+
skip the stop entirely and leave the position unprotected
|
|
35
38
|
|
|
36
39
|
Examples:
|
|
37
40
|
# Take profit: sell 0.5 HYPE when price rises to $40
|
|
@@ -63,6 +66,11 @@ async function main() {
|
|
|
63
66
|
const orderType = args.type as string;
|
|
64
67
|
const limitPriceOverride = args.limit ? parseFloat(args.limit as string) : undefined;
|
|
65
68
|
const slippageBps = args.slippage ? parseInt(args.slippage as string) : 100;
|
|
69
|
+
const execOverride = args.exec as string | undefined;
|
|
70
|
+
if (execOverride && execOverride !== 'market' && execOverride !== 'limit') {
|
|
71
|
+
console.error('Error: --exec must be "market" or "limit"');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
66
74
|
const leverage = args.leverage ? parseInt(args.leverage as string) : undefined;
|
|
67
75
|
const reduceOnly = args.reduce !== 'false'; // Default true
|
|
68
76
|
const dryRun = args.dry as boolean;
|
|
@@ -116,13 +124,18 @@ async function main() {
|
|
|
116
124
|
// TP: use trigger price as limit (favorable)
|
|
117
125
|
limitPrice = triggerPrice;
|
|
118
126
|
} else {
|
|
119
|
-
// SL:
|
|
127
|
+
// SL: slippage band past the trigger — caps the market fill, or is the
|
|
128
|
+
// resting price for a stop-limit
|
|
120
129
|
const slippageMult = slippageBps / 10000;
|
|
121
130
|
limitPrice = isBuy
|
|
122
131
|
? triggerPrice * (1 + slippageMult)
|
|
123
132
|
: triggerPrice * (1 - slippageMult);
|
|
124
133
|
}
|
|
125
134
|
|
|
135
|
+
// SL fires as a market trigger by default (a stop-limit can be gapped
|
|
136
|
+
// over and never fill); TP rests as a limit at the target.
|
|
137
|
+
const isMarket = execOverride ? execOverride === 'market' : tpsl === 'sl';
|
|
138
|
+
|
|
126
139
|
const distanceFromCurrent = ((triggerPrice - currentPrice) / currentPrice) * 100;
|
|
127
140
|
const notional = triggerPrice * size;
|
|
128
141
|
|
|
@@ -134,7 +147,7 @@ async function main() {
|
|
|
134
147
|
console.log(`Size: ${size}`);
|
|
135
148
|
console.log(`Current Price: ${formatUsd(currentPrice)}`);
|
|
136
149
|
console.log(`Trigger Price: ${formatUsd(triggerPrice)} (${distanceFromCurrent >= 0 ? '+' : ''}${distanceFromCurrent.toFixed(2)}%)`);
|
|
137
|
-
console.log(`
|
|
150
|
+
console.log(`Execution: ${isMarket ? `market (fill capped at ${formatUsd(limitPrice)})` : `limit @ ${formatUsd(limitPrice)}`}`);
|
|
138
151
|
console.log(`Reduce Only: ${reduceOnly ? 'Yes' : 'No'}`);
|
|
139
152
|
console.log(`Est. Notional: ${formatUsd(notional)}`);
|
|
140
153
|
|
|
@@ -170,7 +183,8 @@ async function main() {
|
|
|
170
183
|
limitPrice,
|
|
171
184
|
tpsl,
|
|
172
185
|
reduceOnly,
|
|
173
|
-
leverage
|
|
186
|
+
leverage,
|
|
187
|
+
isMarket
|
|
174
188
|
);
|
|
175
189
|
|
|
176
190
|
console.log('\nResult');
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// TWAP (Time-Weighted Average Price) execution using Hyperliquid's native TWAP orders
|
|
3
3
|
|
|
4
4
|
import { getClient } from '../core/client.js';
|
|
5
|
-
import { formatUsd, parseArgs } from '../core/utils.js';
|
|
5
|
+
import { MIN_ORDER_NOTIONAL_USD, formatUsd, parseArgs } from '../core/utils.js';
|
|
6
6
|
|
|
7
7
|
function printUsage() {
|
|
8
8
|
console.log(`
|
|
@@ -90,6 +90,10 @@ async function main() {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
const notional = midPrice * totalSize;
|
|
93
|
+
// Native TWAP fires a sub-order every 30s; each must clear the exchange minimum.
|
|
94
|
+
const sliceCount = durationMinutes * 2;
|
|
95
|
+
const perSliceNotional = notional / sliceCount;
|
|
96
|
+
const sliceBelowMinimum = !reduceOnly && perSliceNotional < MIN_ORDER_NOTIONAL_USD;
|
|
93
97
|
|
|
94
98
|
console.log('Order Details');
|
|
95
99
|
console.log('-------------');
|
|
@@ -99,6 +103,7 @@ async function main() {
|
|
|
99
103
|
console.log(`Current Price: ${formatUsd(midPrice)}`);
|
|
100
104
|
console.log(`Est. Notional: ${formatUsd(notional)}`);
|
|
101
105
|
console.log(`Duration: ${formatDuration(durationMinutes * 60)}`);
|
|
106
|
+
console.log(`Slices: ~${sliceCount} (every 30s, ~${formatUsd(perSliceNotional)} each)`);
|
|
102
107
|
console.log(`Randomize: ${randomize ? 'yes' : 'no'}`);
|
|
103
108
|
console.log(`Reduce Only: ${reduceOnly ? 'yes' : 'no'}`);
|
|
104
109
|
if (leverage) {
|
|
@@ -106,11 +111,19 @@ async function main() {
|
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
if (dryRun) {
|
|
114
|
+
if (sliceBelowMinimum) {
|
|
115
|
+
console.log(`\n⚠️ TWAP slices of ~${formatUsd(perSliceNotional)} fall below the $${MIN_ORDER_NOTIONAL_USD} exchange minimum — shorten the duration or increase the size.`);
|
|
116
|
+
}
|
|
109
117
|
console.log('\nDry run - no order placed.');
|
|
110
118
|
console.log('The exchange will handle order slicing and timing automatically.');
|
|
111
119
|
return;
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
if (sliceBelowMinimum) {
|
|
123
|
+
console.error(`Error: TWAP slices of ~${formatUsd(perSliceNotional)} fall below the $${MIN_ORDER_NOTIONAL_USD} exchange minimum — shorten the duration or increase the size.`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
114
127
|
console.log('\nPlacing native TWAP order...\n');
|
|
115
128
|
|
|
116
129
|
const response = await client.twapOrder(
|