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
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
256
|
+
else if (status?.kind === 'error') {
|
|
257
|
+
placementErrors++;
|
|
258
|
+
console.log(`❌ ${leg} failed: ${status.error}`);
|
|
250
259
|
}
|
|
251
260
|
else {
|
|
252
|
-
|
|
261
|
+
placementErrors++;
|
|
262
|
+
console.log(`⚠️ ${leg} status:`, JSON.stringify(status));
|
|
253
263
|
}
|
|
254
264
|
}
|
|
255
|
-
|
|
256
|
-
|
|
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 (
|
|
268
|
-
console.log(`\n💡
|
|
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
|
-
-
|
|
31
|
-
-
|
|
32
|
-
|
|
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:
|
|
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(`
|
|
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') {
|
package/dist/operations/twap.js
CHANGED
|
@@ -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
|