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