openbroker 1.0.89 → 1.1.1

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,6 +1,7 @@
1
1
  #!/usr/bin/env npx tsx
2
2
  // Chase Order - Follow price with limit orders until filled
3
3
 
4
+ import { fileURLToPath } from 'url';
4
5
  import { getClient } from '../core/client.js';
5
6
  import { formatUsd, parseArgs, sleep } from '../core/utils.js';
6
7
 
@@ -27,212 +28,234 @@ Options:
27
28
  --reduce Reduce-only order
28
29
  --dry Dry run - show chase parameters without executing
29
30
 
30
- Strategy:
31
- - Places ALO (post-only) order at mid ± offset
32
- - If not filled, cancels and replaces closer to mid
33
- - Stops when filled or timeout/max-chase reached
34
- - Uses ALO to ensure maker rebates
35
-
36
31
  Examples:
37
32
  # Chase buy 0.5 ETH with 5 bps offset, 5 min timeout
38
33
  npx tsx scripts/operations/chase.ts --coin ETH --side buy --size 0.5
39
34
 
40
35
  # Chase sell with tighter offset and longer timeout
41
36
  npx tsx scripts/operations/chase.ts --coin BTC --side sell --size 0.1 --offset 2 --timeout 600
42
-
43
- # Quick aggressive chase (1 bps offset, 1 min timeout, 50 bps max chase)
44
- npx tsx scripts/operations/chase.ts --coin SOL --side buy --size 10 --offset 1 --timeout 60 --max-chase 50
45
37
  `);
46
38
  }
47
39
 
48
- async function main() {
49
- const args = parseArgs(process.argv.slice(2));
40
+ export interface ChaseOptions {
41
+ coin: string;
42
+ side: 'buy' | 'sell';
43
+ size: number;
44
+ offsetBps?: number;
45
+ timeoutSec?: number;
46
+ intervalMs?: number;
47
+ maxChaseBps?: number;
48
+ leverage?: number;
49
+ reduceOnly?: boolean;
50
+ dryRun?: boolean;
51
+ verbose?: boolean;
52
+ /** Receives each output line. Defaults to console.log. */
53
+ output?: (line: string) => void;
54
+ }
50
55
 
51
- const coin = args.coin as string;
52
- const side = args.side as string;
53
- const size = parseFloat(args.size as string);
54
- const offsetBps = args.offset ? parseInt(args.offset as string) : 5;
55
- const timeoutSec = args.timeout ? parseInt(args.timeout as string) : 300;
56
- const intervalMs = args.interval ? parseInt(args.interval as string) : 2000;
57
- const maxChaseBps = args['max-chase'] ? parseInt(args['max-chase'] as string) : 100;
58
- const leverage = args.leverage ? parseInt(args.leverage as string) : undefined;
59
- const reduceOnly = args.reduce as boolean;
60
- const dryRun = args.dry as boolean;
56
+ export interface ChaseResult {
57
+ status: 'dry' | 'filled' | 'timeout' | 'max_chase_exceeded';
58
+ iterations: number;
59
+ durationSec: number;
60
+ startMid: number;
61
+ endMid: number;
62
+ }
61
63
 
62
- if (!coin || !side || isNaN(size)) {
63
- printUsage();
64
- process.exit(1);
65
- }
64
+ export async function runChase(opts: ChaseOptions): Promise<ChaseResult> {
65
+ const out = opts.output ?? ((line: string) => console.log(line));
66
+ const offsetBps = opts.offsetBps ?? 5;
67
+ const timeoutSec = opts.timeoutSec ?? 300;
68
+ const intervalMs = opts.intervalMs ?? 2000;
69
+ const maxChaseBps = opts.maxChaseBps ?? 100;
70
+ const isBuy = opts.side === 'buy';
66
71
 
67
- if (side !== 'buy' && side !== 'sell') {
68
- console.error('Error: --side must be "buy" or "sell"');
69
- process.exit(1);
70
- }
72
+ if (opts.size <= 0 || isNaN(opts.size)) throw new Error('size must be positive');
71
73
 
72
- const isBuy = side === 'buy';
73
74
  const client = getClient();
74
-
75
- if (args.verbose) {
76
- client.verbose = true;
75
+ if (opts.verbose) client.verbose = true;
76
+
77
+ out('Open Broker - Chase Order');
78
+ out('=========================\n');
79
+
80
+ const mids = await client.getAllMids();
81
+ const startMid = parseFloat(mids[opts.coin]);
82
+ if (!startMid) throw new Error(`No market data for ${opts.coin}`);
83
+
84
+ const maxChasePrice = isBuy
85
+ ? startMid * (1 + maxChaseBps / 10000)
86
+ : startMid * (1 - maxChaseBps / 10000);
87
+
88
+ out('Chase Parameters');
89
+ out('----------------');
90
+ out(`Coin: ${opts.coin}`);
91
+ out(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
92
+ out(`Size: ${opts.size}`);
93
+ out(`Start Mid: ${formatUsd(startMid)}`);
94
+ out(`Offset: ${offsetBps} bps (${(offsetBps / 100).toFixed(2)}%)`);
95
+ out(`Max Chase: ${maxChaseBps} bps to ${formatUsd(maxChasePrice)}`);
96
+ out(`Timeout: ${timeoutSec}s`);
97
+ out(`Update Rate: ${intervalMs}ms`);
98
+ out(`Order Type: ALO (post-only)`);
99
+
100
+ if (opts.dryRun) {
101
+ out('\nšŸ” Dry run - chase not started');
102
+ return { status: 'dry', iterations: 0, durationSec: 0, startMid, endMid: startMid };
77
103
  }
78
104
 
79
- console.log('Open Broker - Chase Order');
80
- console.log('=========================\n');
81
-
82
- try {
83
- const mids = await client.getAllMids();
84
- const startMid = parseFloat(mids[coin]);
85
- if (!startMid) {
86
- console.error(`Error: No market data for ${coin}`);
87
- process.exit(1);
88
- }
105
+ out('\nChasing...\n');
89
106
 
90
- const maxChasePrice = isBuy
91
- ? startMid * (1 + maxChaseBps / 10000)
92
- : startMid * (1 - maxChaseBps / 10000);
93
-
94
- console.log('Chase Parameters');
95
- console.log('----------------');
96
- console.log(`Coin: ${coin}`);
97
- console.log(`Side: ${isBuy ? 'BUY' : 'SELL'}`);
98
- console.log(`Size: ${size}`);
99
- console.log(`Start Mid: ${formatUsd(startMid)}`);
100
- console.log(`Offset: ${offsetBps} bps (${(offsetBps / 100).toFixed(2)}%)`);
101
- console.log(`Max Chase: ${maxChaseBps} bps to ${formatUsd(maxChasePrice)}`);
102
- console.log(`Timeout: ${timeoutSec}s`);
103
- console.log(`Update Rate: ${intervalMs}ms`);
104
- console.log(`Order Type: ALO (post-only)`);
105
-
106
- if (dryRun) {
107
- console.log('\nšŸ” Dry run - chase not started');
108
- return;
109
- }
107
+ const startTime = Date.now();
108
+ let currentOid: number | null = null;
109
+ let lastPrice: number | null = null;
110
+ let iteration = 0;
111
+ let filled = false;
112
+ let exitReason: 'filled' | 'timeout' | 'max_chase_exceeded' = 'timeout';
110
113
 
111
- console.log('\nChasing...\n');
114
+ while (Date.now() - startTime < timeoutSec * 1000) {
115
+ iteration++;
112
116
 
113
- const startTime = Date.now();
114
- let currentOid: number | null = null;
115
- let lastPrice: number | null = null;
116
- let iteration = 0;
117
- let filled = false;
117
+ const currentMids = await client.getAllMids();
118
+ const currentMid = parseFloat(currentMids[opts.coin]);
118
119
 
119
- while (Date.now() - startTime < timeoutSec * 1000) {
120
- iteration++;
120
+ if (isBuy && currentMid > maxChasePrice) {
121
+ out(`\nāš ļø Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
122
+ exitReason = 'max_chase_exceeded';
123
+ break;
124
+ }
125
+ if (!isBuy && currentMid < maxChasePrice) {
126
+ out(`\nāš ļø Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
127
+ exitReason = 'max_chase_exceeded';
128
+ break;
129
+ }
121
130
 
122
- // Get current mid
123
- const currentMids = await client.getAllMids();
124
- const currentMid = parseFloat(currentMids[coin]);
131
+ const orderPrice = isBuy
132
+ ? currentMid * (1 - offsetBps / 10000)
133
+ : currentMid * (1 + offsetBps / 10000);
125
134
 
126
- // Check max chase limit
127
- if (isBuy && currentMid > maxChasePrice) {
128
- console.log(`\nāš ļø Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
129
- break;
130
- }
131
- if (!isBuy && currentMid < maxChasePrice) {
132
- console.log(`\nāš ļø Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
133
- break;
134
- }
135
+ const priceChanged = !lastPrice || Math.abs(orderPrice - lastPrice) / lastPrice > 0.0001;
135
136
 
136
- // Calculate order price with offset
137
- const orderPrice = isBuy
138
- ? currentMid * (1 - offsetBps / 10000)
139
- : currentMid * (1 + offsetBps / 10000);
140
-
141
- // Check if price moved enough to update
142
- const priceChanged = !lastPrice || Math.abs(orderPrice - lastPrice) / lastPrice > 0.0001;
143
-
144
- if (priceChanged) {
145
- // Cancel existing order if any
146
- if (currentOid !== null) {
147
- try {
148
- await client.cancel(coin, currentOid);
149
- } catch {
150
- // Order might have filled
151
- }
152
- currentOid = null;
137
+ if (priceChanged) {
138
+ if (currentOid !== null) {
139
+ try {
140
+ await client.cancel(opts.coin, currentOid);
141
+ } catch {
142
+ // Order might have filled
153
143
  }
144
+ currentOid = null;
145
+ }
154
146
 
155
- // Check if we got filled while cancelling
156
- const orders = await client.getOpenOrders();
157
- const ourOrder = orders.find(o => o.coin === coin && o.oid === currentOid);
158
- if (!ourOrder && currentOid !== null) {
159
- // Order gone - might have filled
160
- // Check position to confirm
161
- const state = await client.getUserState();
162
- const pos = state.assetPositions.find(p => p.position.coin === coin);
163
- if (pos && Math.abs(parseFloat(pos.position.szi)) >= size * 0.99) {
164
- filled = true;
165
- console.log(`\nāœ… Order filled!`);
166
- break;
167
- }
147
+ const orders = await client.getOpenOrders();
148
+ const ourOrder = orders.find(o => o.coin === opts.coin && o.oid === currentOid);
149
+ if (!ourOrder && currentOid !== null) {
150
+ const state = await client.getUserState();
151
+ const pos = state.assetPositions.find(p => p.position.coin === opts.coin);
152
+ if (pos && Math.abs(parseFloat(pos.position.szi)) >= opts.size * 0.99) {
153
+ filled = true;
154
+ exitReason = 'filled';
155
+ out(`\nāœ… Order filled!`);
156
+ break;
168
157
  }
158
+ }
169
159
 
170
- // Place new order
171
- process.stdout.write(`[${iteration}] Mid: ${formatUsd(currentMid)} → Order: ${formatUsd(orderPrice)}... `);
172
-
173
- const response = await client.limitOrder(coin, isBuy, size, orderPrice, 'Alo', reduceOnly, leverage);
174
-
175
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
176
- const status = response.response.data.statuses[0];
177
- if (status?.resting) {
178
- currentOid = status.resting.oid;
179
- lastPrice = orderPrice;
180
- console.log(`OID: ${currentOid}`);
181
- } else if (status?.filled) {
182
- filled = true;
183
- console.log(`āœ… Filled @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
184
- break;
185
- } else if (status?.error) {
186
- console.log(`āŒ ${status.error}`);
187
- // If ALO rejected (would be taker), try again next iteration
188
- }
189
- } else {
190
- console.log(`āŒ Failed`);
160
+ out(`[${iteration}] Mid: ${formatUsd(currentMid)} → Order: ${formatUsd(orderPrice)}...`);
161
+
162
+ const response = await client.limitOrder(opts.coin, isBuy, opts.size, orderPrice, 'Alo', opts.reduceOnly, opts.leverage);
163
+
164
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
165
+ const status = response.response.data.statuses[0];
166
+ if (status?.resting) {
167
+ currentOid = status.resting.oid;
168
+ lastPrice = orderPrice;
169
+ out(`OID: ${currentOid}`);
170
+ } else if (status?.filled) {
171
+ filled = true;
172
+ exitReason = 'filled';
173
+ out(`āœ… Filled @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
174
+ break;
175
+ } else if (status?.error) {
176
+ out(`āŒ ${status.error}`);
191
177
  }
192
178
  } else {
193
- // Price stable, check if filled
194
- if (currentOid !== null) {
195
- const orders = await client.getOpenOrders();
196
- const ourOrder = orders.find(o => o.oid === currentOid);
197
- if (!ourOrder) {
198
- filled = true;
199
- console.log(`\nāœ… Order filled!`);
200
- break;
201
- }
179
+ out(`āŒ Failed`);
180
+ }
181
+ } else {
182
+ if (currentOid !== null) {
183
+ const orders = await client.getOpenOrders();
184
+ const ourOrder = orders.find(o => o.oid === currentOid);
185
+ if (!ourOrder) {
186
+ filled = true;
187
+ exitReason = 'filled';
188
+ out(`\nāœ… Order filled!`);
189
+ break;
202
190
  }
203
- process.stdout.write('.');
204
191
  }
205
-
206
- await sleep(intervalMs);
207
192
  }
208
193
 
209
- // Cleanup
210
- if (currentOid !== null && !filled) {
211
- console.log(`\nCancelling unfilled order...`);
212
- try {
213
- await client.cancel(coin, currentOid);
214
- console.log(`āœ… Cancelled`);
215
- } catch {
216
- console.log(`āš ļø Could not cancel (may have filled)`);
217
- }
194
+ await sleep(intervalMs);
195
+ }
196
+
197
+ if (currentOid !== null && !filled) {
198
+ out(`\nCancelling unfilled order...`);
199
+ try {
200
+ await client.cancel(opts.coin, currentOid);
201
+ out(`āœ… Cancelled`);
202
+ } catch {
203
+ out(`āš ļø Could not cancel (may have filled)`);
218
204
  }
205
+ }
206
+
207
+ const elapsed = (Date.now() - startTime) / 1000;
208
+ const endMid = parseFloat((await client.getAllMids())[opts.coin]);
209
+ const priceMove = ((endMid - startMid) / startMid) * 10000;
210
+
211
+ out('\n========== Chase Summary ==========');
212
+ out(`Duration: ${elapsed.toFixed(1)}s`);
213
+ out(`Iterations: ${iteration}`);
214
+ out(`Start Mid: ${formatUsd(startMid)}`);
215
+ out(`End Mid: ${formatUsd(endMid)} (${priceMove >= 0 ? '+' : ''}${priceMove.toFixed(1)} bps)`);
216
+ out(`Result: ${filled ? 'āœ… Filled' : 'āŒ Not filled'}`);
217
+
218
+ return { status: exitReason, iterations: iteration, durationSec: elapsed, startMid, endMid };
219
+ }
220
+
221
+ async function main() {
222
+ const args = parseArgs(process.argv.slice(2));
219
223
 
220
- // Summary
221
- const elapsed = (Date.now() - startTime) / 1000;
222
- const endMid = parseFloat((await client.getAllMids())[coin]);
223
- const priceMove = ((endMid - startMid) / startMid) * 10000;
224
+ const coin = args.coin as string;
225
+ const side = args.side as string;
226
+ const size = parseFloat(args.size as string);
224
227
 
225
- console.log('\n========== Chase Summary ==========');
226
- console.log(`Duration: ${elapsed.toFixed(1)}s`);
227
- console.log(`Iterations: ${iteration}`);
228
- console.log(`Start Mid: ${formatUsd(startMid)}`);
229
- console.log(`End Mid: ${formatUsd(endMid)} (${priceMove >= 0 ? '+' : ''}${priceMove.toFixed(1)} bps)`);
230
- console.log(`Result: ${filled ? 'āœ… Filled' : 'āŒ Not filled'}`);
228
+ if (!coin || !side || isNaN(size)) {
229
+ printUsage();
230
+ process.exit(1);
231
+ }
232
+ if (side !== 'buy' && side !== 'sell') {
233
+ console.error('Error: --side must be "buy" or "sell"');
234
+ process.exit(1);
235
+ }
231
236
 
237
+ try {
238
+ await runChase({
239
+ coin,
240
+ side: side as 'buy' | 'sell',
241
+ size,
242
+ offsetBps: args.offset ? parseInt(args.offset as string) : undefined,
243
+ timeoutSec: args.timeout ? parseInt(args.timeout as string) : undefined,
244
+ intervalMs: args.interval ? parseInt(args.interval as string) : undefined,
245
+ maxChaseBps: args['max-chase'] ? parseInt(args['max-chase'] as string) : undefined,
246
+ leverage: args.leverage ? parseInt(args.leverage as string) : undefined,
247
+ reduceOnly: args.reduce as boolean,
248
+ dryRun: args.dry as boolean,
249
+ verbose: args.verbose as boolean,
250
+ });
232
251
  } catch (error) {
233
- console.error('Error:', error);
252
+ console.error('Error:', error instanceof Error ? error.message : error);
234
253
  process.exit(1);
235
254
  }
236
255
  }
237
256
 
238
- main();
257
+ // Only run when invoked as a script — not when imported as a module
258
+ // (e.g. by `openbroker-plugin` via the lib re-export of `runChase`).
259
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
260
+ main();
261
+ }