openbroker 1.0.67 → 1.0.69

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,397 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- // Grid Trading Strategy - Place orders at multiple price levels
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 - Grid Trading
10
- ==========================
11
-
12
- Place buy and sell orders across a price range. Profits from price oscillations
13
- within the range by buying low and selling high repeatedly.
14
-
15
- Usage:
16
- npx tsx scripts/strategies/grid.ts --coin <COIN> --lower <PRICE> --upper <PRICE> --grids <N> --size <SIZE>
17
-
18
- Options:
19
- --coin Asset to trade (e.g., ETH, BTC)
20
- --lower Lower bound of grid (price)
21
- --upper Upper bound of grid (price)
22
- --grids Number of grid levels (default: 10)
23
- --size Size per grid in base asset
24
- --total-size OR total size to distribute across grids
25
- --mode Grid mode: neutral, long, short (default: neutral)
26
- --refresh Refresh interval in seconds to rebalance grid (default: 60)
27
- --duration How long to run in hours (default: runs until stopped)
28
- --dry Dry run - show grid plan without placing orders
29
-
30
- Grid Modes:
31
- neutral Place both buys below and sells above current price
32
- long Only place buy orders (accumulation grid)
33
- short Only place sell orders (distribution grid)
34
-
35
- Examples:
36
- # ETH grid between $3000-$4000 with 10 levels, 0.1 ETH per level
37
- npx tsx scripts/strategies/grid.ts --coin ETH --lower 3000 --upper 4000 --grids 10 --size 0.1
38
-
39
- # BTC accumulation grid (buys only)
40
- npx tsx scripts/strategies/grid.ts --coin BTC --lower 90000 --upper 100000 --grids 5 --size 0.01 --mode long
41
-
42
- # Preview grid setup
43
- npx tsx scripts/strategies/grid.ts --coin ETH --lower 3000 --upper 4000 --grids 10 --size 0.1 --dry
44
-
45
- How it Works:
46
- 1. Calculates price levels evenly spaced between lower and upper bounds
47
- 2. Places buy orders at levels below current price
48
- 3. Places sell orders at levels above current price
49
- 4. Monitors fills and replaces filled orders on the opposite side
50
- 5. Continues until duration ends or manually stopped
51
- `);
52
- }
53
-
54
- interface GridLevel {
55
- price: number;
56
- side: 'buy' | 'sell';
57
- size: number;
58
- oid?: number;
59
- status: 'pending' | 'open' | 'filled' | 'cancelled';
60
- }
61
-
62
- interface GridState {
63
- levels: GridLevel[];
64
- totalBuys: number;
65
- totalSells: number;
66
- realizedPnl: number;
67
- avgBuyPrice: number;
68
- avgSellPrice: number;
69
- }
70
-
71
- async function main() {
72
- const args = parseArgs(process.argv.slice(2));
73
-
74
- const coin = args.coin as string;
75
- const lower = parseFloat(args.lower as string);
76
- const upper = parseFloat(args.upper as string);
77
- const grids = args.grids ? parseInt(args.grids as string) : 10;
78
- const sizePerGrid = args.size ? parseFloat(args.size as string) : undefined;
79
- const totalSize = args['total-size'] ? parseFloat(args['total-size'] as string) : undefined;
80
- const mode = (args.mode as string) || 'neutral';
81
- const refreshInterval = args.refresh ? parseInt(args.refresh as string) : 60;
82
- const durationHours = args.duration ? parseFloat(args.duration as string) : Infinity;
83
- const dryRun = args.dry as boolean;
84
-
85
- if (!coin || isNaN(lower) || isNaN(upper) || (!sizePerGrid && !totalSize)) {
86
- printUsage();
87
- process.exit(1);
88
- }
89
-
90
- if (lower >= upper) {
91
- console.error('Error: --lower must be less than --upper');
92
- process.exit(1);
93
- }
94
-
95
- if (!['neutral', 'long', 'short'].includes(mode)) {
96
- console.error('Error: --mode must be "neutral", "long", or "short"');
97
- process.exit(1);
98
- }
99
-
100
- const size = sizePerGrid || (totalSize! / grids);
101
-
102
- const client = getClient();
103
-
104
- if (args.verbose) {
105
- client.verbose = true;
106
- }
107
-
108
- console.log('Open Broker - Grid Trading');
109
- console.log('==========================\n');
110
-
111
- try {
112
- const mids = await client.getAllMids();
113
- const midPrice = parseFloat(mids[coin]);
114
- if (!midPrice) {
115
- console.error(`Error: No market data for ${coin}`);
116
- process.exit(1);
117
- }
118
-
119
- // Calculate grid levels
120
- const gridSpacing = (upper - lower) / (grids - 1);
121
- const levels: GridLevel[] = [];
122
-
123
- for (let i = 0; i < grids; i++) {
124
- const price = lower + gridSpacing * i;
125
- let side: 'buy' | 'sell';
126
-
127
- if (mode === 'long') {
128
- side = 'buy';
129
- } else if (mode === 'short') {
130
- side = 'sell';
131
- } else {
132
- // Neutral: buys below mid, sells above mid
133
- side = price < midPrice ? 'buy' : 'sell';
134
- }
135
-
136
- levels.push({
137
- price,
138
- side,
139
- size,
140
- status: 'pending',
141
- });
142
- }
143
-
144
- const buyLevels = levels.filter(l => l.side === 'buy');
145
- const sellLevels = levels.filter(l => l.side === 'sell');
146
- const totalNotional = levels.reduce((sum, l) => sum + l.price * l.size, 0);
147
-
148
- console.log('Grid Configuration');
149
- console.log('------------------');
150
- console.log(`Coin: ${coin}`);
151
- console.log(`Current Price: ${formatUsd(midPrice)}`);
152
- console.log(`Range: ${formatUsd(lower)} - ${formatUsd(upper)}`);
153
- console.log(`Grid Spacing: ${formatUsd(gridSpacing)} (${((gridSpacing / midPrice) * 100).toFixed(2)}%)`);
154
- console.log(`Grid Levels: ${grids}`);
155
- console.log(`Size/Level: ${size} ${coin}`);
156
- console.log(`Total Size: ${size * grids} ${coin}`);
157
- console.log(`Total Notional: ~${formatUsd(totalNotional)}`);
158
- console.log(`Mode: ${mode.toUpperCase()}`);
159
- console.log(`Buy Orders: ${buyLevels.length}`);
160
- console.log(`Sell Orders: ${sellLevels.length}`);
161
-
162
- console.log('\nGrid Levels');
163
- console.log('-----------');
164
- for (let i = levels.length - 1; i >= 0; i--) {
165
- const level = levels[i];
166
- const marker = Math.abs(level.price - midPrice) < gridSpacing / 2 ? ' <-- current' : '';
167
- const sideLabel = level.side === 'buy' ? 'BUY ' : 'SELL';
168
- console.log(` ${sideLabel} @ ${formatUsd(level.price)} (${size} ${coin})${marker}`);
169
- }
170
-
171
- // Profit estimation
172
- const profitPerRoundTrip = gridSpacing * size;
173
- console.log('\nProfit Estimation (per round trip)');
174
- console.log('-----------------------------------');
175
- console.log(`Profit/Grid: ${formatUsd(profitPerRoundTrip)}`);
176
- console.log(`If all grids: ${formatUsd(profitPerRoundTrip * (grids - 1))}`);
177
-
178
- if (dryRun) {
179
- console.log('\n--- Dry run complete ---');
180
- return;
181
- }
182
-
183
- // Place initial orders
184
- console.log('\nPlacing grid orders...\n');
185
-
186
- const state: GridState = {
187
- levels,
188
- totalBuys: 0,
189
- totalSells: 0,
190
- realizedPnl: 0,
191
- avgBuyPrice: 0,
192
- avgSellPrice: 0,
193
- };
194
-
195
- for (const level of levels) {
196
- // Skip levels too close to current price (within 0.1%)
197
- const distance = Math.abs(level.price - midPrice) / midPrice;
198
- if (distance < 0.001) {
199
- console.log(` Skipping level @ ${formatUsd(level.price)} (too close to current price)`);
200
- level.status = 'cancelled';
201
- continue;
202
- }
203
-
204
- const response = await client.limitOrder(
205
- coin,
206
- level.side === 'buy',
207
- level.size,
208
- level.price,
209
- 'Gtc',
210
- false
211
- );
212
-
213
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
214
- const status = response.response.data.statuses[0];
215
- if (status?.resting) {
216
- level.oid = status.resting.oid;
217
- level.status = 'open';
218
- console.log(` ${level.side.toUpperCase()} @ ${formatUsd(level.price)} - OID: ${level.oid}`);
219
- } else if (status?.filled) {
220
- level.status = 'filled';
221
- const fillPrice = parseFloat(status.filled.avgPx);
222
- console.log(` ${level.side.toUpperCase()} @ ${formatUsd(level.price)} - FILLED @ ${formatUsd(fillPrice)}`);
223
-
224
- // Track for PnL
225
- if (level.side === 'buy') {
226
- state.totalBuys += level.size;
227
- state.avgBuyPrice = ((state.avgBuyPrice * (state.totalBuys - level.size)) + (fillPrice * level.size)) / state.totalBuys;
228
- } else {
229
- state.totalSells += level.size;
230
- state.avgSellPrice = ((state.avgSellPrice * (state.totalSells - level.size)) + (fillPrice * level.size)) / state.totalSells;
231
- }
232
- } else if (status?.error) {
233
- level.status = 'cancelled';
234
- console.log(` ${level.side.toUpperCase()} @ ${formatUsd(level.price)} - ERROR: ${status.error}`);
235
- }
236
- } else {
237
- level.status = 'cancelled';
238
- console.log(` ${level.side.toUpperCase()} @ ${formatUsd(level.price)} - FAILED`);
239
- }
240
-
241
- await sleep(100); // Small delay between orders
242
- }
243
-
244
- const openOrders = levels.filter(l => l.status === 'open').length;
245
- console.log(`\nGrid initialized with ${openOrders} open orders.`);
246
-
247
- // Monitoring loop
248
- if (durationHours !== Infinity) {
249
- const endTime = Date.now() + durationHours * 3600 * 1000;
250
-
251
- console.log(`\nMonitoring grid for ${durationHours} hours...`);
252
- console.log(`(Press Ctrl+C to stop and cancel orders)\n`);
253
-
254
- // Handle graceful shutdown
255
- process.on('SIGINT', async () => {
256
- console.log('\n\nShutting down - cancelling all grid orders...');
257
- await cancelAllGridOrders(client, coin, state.levels);
258
- printSummary(state);
259
- process.exit(0);
260
- });
261
-
262
- while (Date.now() < endTime) {
263
- await sleep(refreshInterval * 1000);
264
-
265
- // Check for filled orders
266
- const openOrders = await client.getOpenOrders();
267
- const openOids = new Set(openOrders.filter(o => o.coin === coin).map(o => o.oid));
268
-
269
- for (const level of state.levels) {
270
- if (level.status === 'open' && level.oid && !openOids.has(level.oid)) {
271
- // Order was filled
272
- level.status = 'filled';
273
- console.log(`[${new Date().toLocaleTimeString()}] ${level.side.toUpperCase()} FILLED @ ${formatUsd(level.price)}`);
274
-
275
- // Update tracking
276
- if (level.side === 'buy') {
277
- state.totalBuys += level.size;
278
- state.avgBuyPrice = level.price;
279
- } else {
280
- state.totalSells += level.size;
281
- state.avgSellPrice = level.price;
282
-
283
- // Calculate realized PnL when we sell
284
- if (state.avgBuyPrice > 0) {
285
- const pnl = (level.price - state.avgBuyPrice) * level.size;
286
- state.realizedPnl += pnl;
287
- }
288
- }
289
-
290
- // Place opposite order at next level
291
- if (mode === 'neutral') {
292
- const oppositePrice = level.side === 'buy'
293
- ? level.price + gridSpacing
294
- : level.price - gridSpacing;
295
-
296
- if (oppositePrice >= lower && oppositePrice <= upper) {
297
- const oppositeSide = level.side === 'buy' ? 'sell' : 'buy';
298
- const response = await client.limitOrder(
299
- coin,
300
- oppositeSide === 'buy',
301
- level.size,
302
- oppositePrice,
303
- 'Gtc',
304
- false
305
- );
306
-
307
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
308
- const status = response.response.data.statuses[0];
309
- if (status?.resting) {
310
- // Add new level
311
- const newLevel: GridLevel = {
312
- price: oppositePrice,
313
- side: oppositeSide,
314
- size: level.size,
315
- oid: status.resting.oid,
316
- status: 'open',
317
- };
318
- state.levels.push(newLevel);
319
- console.log(` -> Placed ${oppositeSide.toUpperCase()} @ ${formatUsd(oppositePrice)} (OID: ${newLevel.oid})`);
320
- }
321
- }
322
- }
323
- }
324
- }
325
- }
326
-
327
- // Status update
328
- const currentOpen = state.levels.filter(l => l.status === 'open').length;
329
- const currentFilled = state.levels.filter(l => l.status === 'filled').length;
330
- const newMid = parseFloat((await client.getAllMids())[coin]);
331
- console.log(`[${new Date().toLocaleTimeString()}] Price: ${formatUsd(newMid)} | Open: ${currentOpen} | Filled: ${currentFilled} | PnL: ${formatUsd(state.realizedPnl)}`);
332
- }
333
-
334
- // End of duration - cancel remaining orders
335
- console.log('\nDuration ended. Cancelling remaining orders...');
336
- await cancelAllGridOrders(client, coin, state.levels);
337
- printSummary(state);
338
- } else {
339
- console.log(`\nGrid is running. Press Ctrl+C to stop and cancel orders.`);
340
- console.log(`Refresh interval: ${refreshInterval}s\n`);
341
-
342
- // Handle graceful shutdown
343
- process.on('SIGINT', async () => {
344
- console.log('\n\nShutting down - cancelling all grid orders...');
345
- await cancelAllGridOrders(client, coin, state.levels);
346
- printSummary(state);
347
- process.exit(0);
348
- });
349
-
350
- // Keep running
351
- while (true) {
352
- await sleep(refreshInterval * 1000);
353
-
354
- const openOrders = await client.getOpenOrders();
355
- const openOids = new Set(openOrders.filter(o => o.coin === coin).map(o => o.oid));
356
- const currentOpen = state.levels.filter(l => l.status === 'open' && l.oid && openOids.has(l.oid)).length;
357
- const newMid = parseFloat((await client.getAllMids())[coin]);
358
-
359
- console.log(`[${new Date().toLocaleTimeString()}] Price: ${formatUsd(newMid)} | Open orders: ${currentOpen} | PnL: ${formatUsd(state.realizedPnl)}`);
360
- }
361
- }
362
-
363
- } catch (error) {
364
- console.error('Error:', error);
365
- process.exit(1);
366
- }
367
- }
368
-
369
- async function cancelAllGridOrders(
370
- client: ReturnType<typeof getClient>,
371
- coin: string,
372
- levels: GridLevel[]
373
- ): Promise<void> {
374
- for (const level of levels) {
375
- if (level.status === 'open' && level.oid) {
376
- try {
377
- await client.cancel(coin, level.oid);
378
- level.status = 'cancelled';
379
- console.log(` Cancelled ${level.side.toUpperCase()} @ ${formatUsd(level.price)}`);
380
- } catch {
381
- // Ignore errors - order may have been filled
382
- }
383
- }
384
- }
385
- }
386
-
387
- function printSummary(state: GridState): void {
388
- console.log('\n========== Grid Summary ==========');
389
- console.log(`Total Buys: ${state.totalBuys.toFixed(6)}`);
390
- console.log(`Total Sells: ${state.totalSells.toFixed(6)}`);
391
- console.log(`Avg Buy Price: ${state.avgBuyPrice > 0 ? formatUsd(state.avgBuyPrice) : 'N/A'}`);
392
- console.log(`Avg Sell Price: ${state.avgSellPrice > 0 ? formatUsd(state.avgSellPrice) : 'N/A'}`);
393
- console.log(`Realized PnL: ${formatUsd(state.realizedPnl)}`);
394
- console.log(`Net Position: ${(state.totalBuys - state.totalSells).toFixed(6)}`);
395
- }
396
-
397
- main();