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.
- package/SKILL.md +102 -57
- package/bin/cli.ts +2 -14
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -9
- package/scripts/auto/cli.ts +114 -20
- package/scripts/auto/examples/dca.ts +72 -0
- package/scripts/auto/examples/funding-arb.ts +98 -0
- package/scripts/auto/examples/grid.ts +135 -0
- package/scripts/auto/examples/mm-maker.ts +125 -0
- package/scripts/auto/examples/mm-spread.ts +115 -0
- package/scripts/auto/loader.ts +47 -1
- package/scripts/auto/runtime.ts +13 -0
- package/scripts/auto/types.ts +14 -0
- package/scripts/plugin/tools.ts +20 -7
- package/scripts/strategies/dca.ts +0 -292
- package/scripts/strategies/funding-arb.ts +0 -352
- package/scripts/strategies/grid.ts +0 -397
- package/scripts/strategies/mm-maker.ts +0 -411
- package/scripts/strategies/mm-spread.ts +0 -402
|
@@ -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();
|