openbroker 1.0.33

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,263 @@
1
+ #!/usr/bin/env npx tsx
2
+ // Scale In/Out - Place a grid of limit orders
3
+
4
+ import { getClient } from '../core/client.js';
5
+ import { formatUsd, parseArgs, sleep } from '../core/utils.js';
6
+
7
+ function printUsage() {
8
+ console.log(`
9
+ Open Broker - Scale In/Out
10
+ ==========================
11
+
12
+ Place a grid of limit orders to scale into or out of a position.
13
+ Orders are distributed across price levels based on the specified range and distribution.
14
+
15
+ Usage:
16
+ npx tsx scripts/operations/scale.ts --coin <COIN> --side <buy|sell> --size <SIZE> --levels <N> --range <PCT>
17
+
18
+ Options:
19
+ --coin Asset to trade (e.g., ETH, BTC)
20
+ --side Order side: buy or sell
21
+ --size Total order size in base asset
22
+ --levels Number of price levels (orders)
23
+ --range Price range from current mid (e.g., 2 for ±2%)
24
+ --distribution Size distribution: linear, exponential, or flat (default: linear)
25
+ - linear: more size at better prices
26
+ - exponential: much more size at better prices
27
+ - flat: equal size at all levels
28
+ --reduce Reduce-only orders (for scaling out of position)
29
+ --tif Time in force: GTC, ALO (default: GTC)
30
+ --dry Dry run - show order plan without executing
31
+
32
+ Examples:
33
+ # Scale into 1 ETH with 5 buy orders, 2% below current price
34
+ npx tsx scripts/operations/scale.ts --coin ETH --side buy --size 1 --levels 5 --range 2
35
+
36
+ # Scale out of 0.5 BTC with 4 sell orders, 3% above current price (reduce-only)
37
+ npx tsx scripts/operations/scale.ts --coin BTC --side sell --size 0.5 --levels 4 --range 3 --reduce
38
+
39
+ # Use exponential distribution for more aggressive scaling
40
+ npx tsx scripts/operations/scale.ts --coin ETH --side buy --size 2 --levels 8 --range 5 --distribution exponential
41
+ `);
42
+ }
43
+
44
+ interface OrderLevel {
45
+ level: number;
46
+ price: number;
47
+ size: number;
48
+ distanceFromMid: number;
49
+ }
50
+
51
+ function calculateLevels(
52
+ midPrice: number,
53
+ isBuy: boolean,
54
+ totalSize: number,
55
+ numLevels: number,
56
+ rangePct: number,
57
+ distribution: 'linear' | 'exponential' | 'flat'
58
+ ): OrderLevel[] {
59
+ const levels: OrderLevel[] = [];
60
+
61
+ // Calculate weights based on distribution
62
+ let weights: number[] = [];
63
+ for (let i = 0; i < numLevels; i++) {
64
+ switch (distribution) {
65
+ case 'flat':
66
+ weights.push(1);
67
+ break;
68
+ case 'linear':
69
+ weights.push(i + 1); // 1, 2, 3, 4, 5... (more at worse prices = better for buyer)
70
+ break;
71
+ case 'exponential':
72
+ weights.push(Math.pow(2, i)); // 1, 2, 4, 8, 16...
73
+ break;
74
+ }
75
+ }
76
+
77
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
78
+
79
+ // Calculate price levels
80
+ for (let i = 0; i < numLevels; i++) {
81
+ // Distance increases with level (i=0 is closest to mid)
82
+ const distancePct = ((i + 1) / numLevels) * rangePct;
83
+
84
+ // Buy orders go below mid, sell orders go above
85
+ const price = isBuy
86
+ ? midPrice * (1 - distancePct / 100)
87
+ : midPrice * (1 + distancePct / 100);
88
+
89
+ const size = (weights[i] / totalWeight) * totalSize;
90
+
91
+ levels.push({
92
+ level: i + 1,
93
+ price,
94
+ size,
95
+ distanceFromMid: distancePct,
96
+ });
97
+ }
98
+
99
+ return levels;
100
+ }
101
+
102
+ async function main() {
103
+ const args = parseArgs(process.argv.slice(2));
104
+
105
+ const coin = args.coin as string;
106
+ const side = args.side as string;
107
+ const totalSize = parseFloat(args.size as string);
108
+ const numLevels = parseInt(args.levels as string);
109
+ const rangePct = parseFloat(args.range as string);
110
+ const distribution = (args.distribution as string || 'linear') as 'linear' | 'exponential' | 'flat';
111
+ const reduceOnly = args.reduce as boolean;
112
+ const tifArg = ((args.tif as string)?.toUpperCase() || 'GTC');
113
+ const dryRun = args.dry as boolean;
114
+
115
+ if (!coin || !side || isNaN(totalSize) || isNaN(numLevels) || isNaN(rangePct)) {
116
+ printUsage();
117
+ process.exit(1);
118
+ }
119
+
120
+ if (side !== 'buy' && side !== 'sell') {
121
+ console.error('Error: --side must be "buy" or "sell"');
122
+ process.exit(1);
123
+ }
124
+
125
+ if (!['linear', 'exponential', 'flat'].includes(distribution)) {
126
+ console.error('Error: --distribution must be linear, exponential, or flat');
127
+ process.exit(1);
128
+ }
129
+
130
+ // Map uppercase CLI input to Pascal case for SDK
131
+ const tifMap: Record<string, 'Gtc' | 'Alo'> = {
132
+ 'GTC': 'Gtc',
133
+ 'ALO': 'Alo'
134
+ };
135
+
136
+ const tif = tifMap[tifArg];
137
+ if (!tif) {
138
+ console.error('Error: --tif must be GTC or ALO');
139
+ process.exit(1);
140
+ }
141
+
142
+ const isBuy = side === 'buy';
143
+ const client = getClient();
144
+
145
+ if (args.verbose) {
146
+ client.verbose = true;
147
+ }
148
+
149
+ console.log('Open Broker - Scale In/Out');
150
+ console.log('==========================\n');
151
+
152
+ try {
153
+ const mids = await client.getAllMids();
154
+ const midPrice = parseFloat(mids[coin]);
155
+ if (!midPrice) {
156
+ console.error(`Error: No market data for ${coin}`);
157
+ process.exit(1);
158
+ }
159
+
160
+ const levels = calculateLevels(midPrice, isBuy, totalSize, numLevels, rangePct, distribution);
161
+ const notional = levels.reduce((sum, l) => sum + l.price * l.size, 0);
162
+ const avgPrice = notional / totalSize;
163
+
164
+ console.log('Order Plan');
165
+ console.log('----------');
166
+ console.log(`Coin: ${coin}`);
167
+ console.log(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
168
+ console.log(`Total Size: ${totalSize}`);
169
+ console.log(`Current Mid: ${formatUsd(midPrice)}`);
170
+ console.log(`Levels: ${numLevels}`);
171
+ console.log(`Range: ${rangePct}% ${isBuy ? 'below' : 'above'} mid`);
172
+ console.log(`Distribution: ${distribution}`);
173
+ console.log(`Time in Force: ${tif}`);
174
+ console.log(`Reduce Only: ${reduceOnly ? 'Yes' : 'No'}`);
175
+ console.log(`Est. Notional: ${formatUsd(notional)}`);
176
+ console.log(`Avg Price: ${formatUsd(avgPrice)}`);
177
+
178
+ console.log('\nOrder Grid');
179
+ console.log('----------');
180
+ console.log('Level | Price | Size | Distance');
181
+ console.log('------|--------------|------------|----------');
182
+
183
+ for (const level of levels) {
184
+ console.log(
185
+ ` ${level.level.toString().padStart(2)} | ` +
186
+ `${formatUsd(level.price).padStart(12)} | ` +
187
+ `${level.size.toFixed(6).padStart(10)} | ` +
188
+ `${level.distanceFromMid.toFixed(2)}%`
189
+ );
190
+ }
191
+
192
+ if (dryRun) {
193
+ console.log('\n🔍 Dry run - orders not placed');
194
+ return;
195
+ }
196
+
197
+ console.log('\nPlacing orders...\n');
198
+
199
+ const results: { level: number; oid?: number; error?: string }[] = [];
200
+
201
+ for (const level of levels) {
202
+ process.stdout.write(`Level ${level.level}: ${formatUsd(level.price)} x ${level.size.toFixed(6)}... `);
203
+
204
+ try {
205
+ const response = await client.limitOrder(
206
+ coin,
207
+ isBuy,
208
+ level.size,
209
+ level.price,
210
+ tif,
211
+ reduceOnly
212
+ );
213
+
214
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
215
+ const status = response.response.data.statuses[0];
216
+ if (status?.resting) {
217
+ console.log(`✅ OID: ${status.resting.oid}`);
218
+ results.push({ level: level.level, oid: status.resting.oid });
219
+ } else if (status?.filled) {
220
+ console.log(`✅ Filled immediately @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
221
+ results.push({ level: level.level, oid: status.filled.oid });
222
+ } else if (status?.error) {
223
+ console.log(`❌ ${status.error}`);
224
+ results.push({ level: level.level, error: status.error });
225
+ } else {
226
+ console.log(`⚠️ Unknown status`);
227
+ results.push({ level: level.level, error: 'Unknown status' });
228
+ }
229
+ } else {
230
+ const error = typeof response.response === 'string' ? response.response : 'Failed';
231
+ console.log(`❌ ${error}`);
232
+ results.push({ level: level.level, error });
233
+ }
234
+ } catch (err) {
235
+ const error = err instanceof Error ? err.message : String(err);
236
+ console.log(`❌ ${error}`);
237
+ results.push({ level: level.level, error });
238
+ }
239
+
240
+ // Small delay between orders
241
+ await sleep(100);
242
+ }
243
+
244
+ // Summary
245
+ const successful = results.filter(r => r.oid).length;
246
+ const failed = results.filter(r => r.error).length;
247
+
248
+ console.log('\n========== Summary ==========');
249
+ console.log(`Orders Placed: ${successful}/${numLevels}`);
250
+ if (failed > 0) {
251
+ console.log(`Failed: ${failed}`);
252
+ }
253
+ if (successful > 0) {
254
+ console.log(`Order IDs: ${results.filter(r => r.oid).map(r => r.oid).join(', ')}`);
255
+ }
256
+
257
+ } catch (error) {
258
+ console.error('Error:', error);
259
+ process.exit(1);
260
+ }
261
+ }
262
+
263
+ main();
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env npx tsx
2
+ // Set Take Profit and/or Stop Loss on an existing position
3
+
4
+ import { getClient } from '../core/client.js';
5
+ import { formatUsd, parseArgs, sleep } from '../core/utils.js';
6
+
7
+ function printUsage() {
8
+ console.log(`
9
+ Open Broker - Set TP/SL
10
+ =======================
11
+
12
+ Add take profit and/or stop loss orders to an existing position.
13
+ Uses trigger orders that execute when price reaches the target.
14
+
15
+ Usage:
16
+ npx tsx scripts/operations/set-tpsl.ts --coin <COIN> [--tp <PRICE>] [--sl <PRICE>]
17
+
18
+ Options:
19
+ --coin Asset with open position (e.g., ETH, BTC, HYPE)
20
+ --tp Take profit trigger price
21
+ --sl Stop loss trigger price
22
+ --size Size to protect (default: full position size)
23
+ --sl-slippage Stop loss slippage in bps (default: 100 = 1%)
24
+ --dry Dry run - show orders without placing
25
+ --verbose Show debug output
26
+
27
+ Price Formats:
28
+ --tp 40 Absolute price ($40)
29
+ --tp +10% Percentage above entry price
30
+ --sl -5% Percentage below entry price (for longs)
31
+ --sl entry Stop loss at entry price (breakeven)
32
+
33
+ Examples:
34
+ # Set TP at $40 and SL at $30 on HYPE long
35
+ npx tsx scripts/operations/set-tpsl.ts --coin HYPE --tp 40 --sl 30
36
+
37
+ # Set TP at +10% from entry, SL at entry (breakeven)
38
+ npx tsx scripts/operations/set-tpsl.ts --coin HYPE --tp +10% --sl entry
39
+
40
+ # Set only stop loss at -5% from entry
41
+ npx tsx scripts/operations/set-tpsl.ts --coin ETH --sl -5%
42
+
43
+ # Set TP/SL on partial position
44
+ npx tsx scripts/operations/set-tpsl.ts --coin ETH --tp 4000 --sl 3500 --size 0.5
45
+
46
+ How Trigger Orders Work:
47
+ - TP/SL are trigger orders, NOT regular limit orders
48
+ - They sit dormant until price reaches the trigger level
49
+ - Once triggered, they execute as limit orders
50
+ - These are reduce-only orders (close position, don't reverse)
51
+ - SL has slippage buffer to ensure fill in fast markets
52
+ `);
53
+ }
54
+
55
+ function parsePrice(input: string, entryPrice: number, isLong: boolean): number | null {
56
+ if (!input) return null;
57
+
58
+ // Handle "entry" keyword for breakeven
59
+ if (input.toLowerCase() === 'entry') {
60
+ return entryPrice;
61
+ }
62
+
63
+ // Handle percentage format: +10%, -5%
64
+ const pctMatch = input.match(/^([+-]?)(\d+(?:\.\d+)?)%$/);
65
+ if (pctMatch) {
66
+ const sign = pctMatch[1] || '+';
67
+ const pct = parseFloat(pctMatch[2]) / 100;
68
+
69
+ if (sign === '+') {
70
+ return entryPrice * (1 + pct);
71
+ } else {
72
+ return entryPrice * (1 - pct);
73
+ }
74
+ }
75
+
76
+ // Handle absolute price
77
+ const price = parseFloat(input);
78
+ if (!isNaN(price) && price > 0) {
79
+ return price;
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ async function main() {
86
+ const args = parseArgs(process.argv.slice(2));
87
+
88
+ if (args.help) {
89
+ printUsage();
90
+ process.exit(0);
91
+ }
92
+
93
+ const coin = args.coin as string;
94
+ const tpInput = args.tp as string | undefined;
95
+ const slInput = args.sl as string | undefined;
96
+ const sizeOverride = args.size ? parseFloat(args.size as string) : undefined;
97
+ const slSlippage = args['sl-slippage'] ? parseInt(args['sl-slippage'] as string) : 100;
98
+ const dryRun = args.dry as boolean;
99
+
100
+ if (!coin) {
101
+ printUsage();
102
+ process.exit(1);
103
+ }
104
+
105
+ if (!tpInput && !slInput) {
106
+ console.error('Error: Must specify at least --tp or --sl');
107
+ process.exit(1);
108
+ }
109
+
110
+ const client = getClient();
111
+
112
+ if (args.verbose) {
113
+ client.verbose = true;
114
+ }
115
+
116
+ console.log('Open Broker - Set TP/SL');
117
+ console.log('=======================\n');
118
+
119
+ try {
120
+ // Get current position
121
+ const userState = await client.getUserState();
122
+ const position = userState.assetPositions.find(p => p.position.coin === coin);
123
+
124
+ if (!position) {
125
+ console.error(`Error: No open position for ${coin}`);
126
+ console.log('\nYour positions:');
127
+ for (const pos of userState.assetPositions) {
128
+ const size = parseFloat(pos.position.szi);
129
+ if (Math.abs(size) > 0) {
130
+ console.log(` ${pos.position.coin}: ${size > 0 ? 'LONG' : 'SHORT'} ${Math.abs(size)}`);
131
+ }
132
+ }
133
+ process.exit(1);
134
+ }
135
+
136
+ const posSize = parseFloat(position.position.szi);
137
+ const entryPrice = parseFloat(position.position.entryPx);
138
+ const isLong = posSize > 0;
139
+ const absSize = Math.abs(posSize);
140
+ const size = sizeOverride ?? absSize;
141
+
142
+ // Get current price
143
+ const mids = await client.getAllMids();
144
+ const currentPrice = parseFloat(mids[coin]);
145
+
146
+ // Parse TP and SL prices
147
+ const tpPrice = tpInput ? parsePrice(tpInput, entryPrice, isLong) : null;
148
+ const slPrice = slInput ? parsePrice(slInput, entryPrice, isLong) : null;
149
+
150
+ if (tpInput && tpPrice === null) {
151
+ console.error(`Error: Invalid TP price format: ${tpInput}`);
152
+ console.log('Use absolute price (e.g., 40), percentage (e.g., +10%), or "entry"');
153
+ process.exit(1);
154
+ }
155
+
156
+ if (slInput && slPrice === null) {
157
+ console.error(`Error: Invalid SL price format: ${slInput}`);
158
+ console.log('Use absolute price (e.g., 35), percentage (e.g., -5%), or "entry"');
159
+ process.exit(1);
160
+ }
161
+
162
+ // Validate TP/SL make sense for position direction
163
+ if (isLong) {
164
+ if (tpPrice && tpPrice <= currentPrice) {
165
+ console.warn(`⚠️ Warning: TP (${formatUsd(tpPrice)}) is at or below current price (${formatUsd(currentPrice)})`);
166
+ console.warn(' For LONG positions, TP should be above current price');
167
+ }
168
+ if (slPrice && slPrice >= currentPrice) {
169
+ console.warn(`⚠️ Warning: SL (${formatUsd(slPrice)}) is at or above current price (${formatUsd(currentPrice)})`);
170
+ console.warn(' For LONG positions, SL should be below current price');
171
+ }
172
+ } else {
173
+ if (tpPrice && tpPrice >= currentPrice) {
174
+ console.warn(`⚠️ Warning: TP (${formatUsd(tpPrice)}) is at or above current price (${formatUsd(currentPrice)})`);
175
+ console.warn(' For SHORT positions, TP should be below current price');
176
+ }
177
+ if (slPrice && slPrice <= currentPrice) {
178
+ console.warn(`⚠️ Warning: SL (${formatUsd(slPrice)}) is at or below current price (${formatUsd(currentPrice)})`);
179
+ console.warn(' For SHORT positions, SL should be above current price');
180
+ }
181
+ }
182
+
183
+ // Calculate risk/reward
184
+ let tpDistance = 0, slDistance = 0, riskReward = 0;
185
+ if (tpPrice) {
186
+ tpDistance = isLong
187
+ ? (tpPrice - entryPrice) / entryPrice * 100
188
+ : (entryPrice - tpPrice) / entryPrice * 100;
189
+ }
190
+ if (slPrice) {
191
+ slDistance = isLong
192
+ ? (entryPrice - slPrice) / entryPrice * 100
193
+ : (slPrice - entryPrice) / entryPrice * 100;
194
+ }
195
+ if (tpDistance > 0 && slDistance > 0) {
196
+ riskReward = tpDistance / slDistance;
197
+ }
198
+
199
+ console.log('Current Position');
200
+ console.log('----------------');
201
+ console.log(`Coin: ${coin}`);
202
+ console.log(`Direction: ${isLong ? 'LONG' : 'SHORT'}`);
203
+ console.log(`Size: ${absSize}`);
204
+ console.log(`Entry Price: ${formatUsd(entryPrice)}`);
205
+ console.log(`Current Price: ${formatUsd(currentPrice)}`);
206
+ console.log(`Unrealized: ${formatUsd(parseFloat(position.position.unrealizedPnl))}`);
207
+
208
+ console.log('\nOrders to Place');
209
+ console.log('---------------');
210
+ if (tpPrice) {
211
+ const tpSide = isLong ? 'SELL' : 'BUY';
212
+ console.log(`Take Profit: ${tpSide} ${size} @ ${formatUsd(tpPrice)} (+${tpDistance.toFixed(2)}% from entry)`);
213
+ }
214
+ if (slPrice) {
215
+ const slSide = isLong ? 'SELL' : 'BUY';
216
+ const slLimitPrice = isLong
217
+ ? slPrice * (1 - slSlippage / 10000)
218
+ : slPrice * (1 + slSlippage / 10000);
219
+ console.log(`Stop Loss: ${slSide} ${size} @ ${formatUsd(slPrice)} trigger, ${formatUsd(slLimitPrice)} limit (-${slDistance.toFixed(2)}%)`);
220
+ }
221
+ if (riskReward > 0) {
222
+ console.log(`Risk/Reward: 1:${riskReward.toFixed(2)}`);
223
+ }
224
+
225
+ // Potential outcomes
226
+ const potentialProfit = tpPrice ? Math.abs(tpPrice - entryPrice) * size : 0;
227
+ const potentialLoss = slPrice ? Math.abs(entryPrice - slPrice) * size : 0;
228
+ console.log('\nPotential Outcomes');
229
+ console.log('------------------');
230
+ if (tpPrice) console.log(`If TP hits: +${formatUsd(potentialProfit)}`);
231
+ if (slPrice) console.log(`If SL hits: -${formatUsd(potentialLoss)}`);
232
+
233
+ if (dryRun) {
234
+ console.log('\n🔍 Dry run - orders not placed');
235
+ return;
236
+ }
237
+
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
+ }
259
+
260
+ await sleep(200);
261
+ }
262
+
263
+ // Place Stop Loss
264
+ let slOid: number | null = null;
265
+ if (slPrice) {
266
+ const slSide = !isLong; // Opposite of position direction
267
+ const response = await client.stopLoss(coin, slSide, size, slPrice, slSlippage);
268
+
269
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
270
+ const status = response.response.data.statuses[0];
271
+ if (status?.resting) {
272
+ slOid = status.resting.oid;
273
+ console.log(`✅ Stop Loss placed @ ${formatUsd(slPrice)} (OID: ${slOid})`);
274
+ } else if (status?.error) {
275
+ console.log(`❌ SL failed: ${status.error}`);
276
+ } else {
277
+ console.log(`⚠️ SL status:`, JSON.stringify(status));
278
+ }
279
+ } else {
280
+ console.log(`❌ SL failed: ${typeof response.response === 'string' ? response.response : 'Unknown error'}`);
281
+ }
282
+ }
283
+
284
+ // Summary
285
+ console.log('\n========== Summary ==========');
286
+ console.log(`Position: ${isLong ? 'LONG' : 'SHORT'} ${absSize} ${coin}`);
287
+ console.log(`Entry: ${formatUsd(entryPrice)}`);
288
+ if (tpOid) console.log(`Take Profit: ${formatUsd(tpPrice!)} (OID: ${tpOid})`);
289
+ if (slOid) console.log(`Stop Loss: ${formatUsd(slPrice!)} (OID: ${slOid})`);
290
+
291
+ if (tpOid && slOid) {
292
+ console.log(`\n💡 Tip: When one order fills, cancel the other manually:`);
293
+ console.log(` npx tsx scripts/operations/cancel.ts --coin ${coin} --oid <OID>`);
294
+ }
295
+
296
+ } catch (error) {
297
+ console.error('Error:', error);
298
+ process.exit(1);
299
+ }
300
+ }
301
+
302
+ main();