openbroker 1.9.4 → 1.9.6

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +21 -4
  3. package/SKILL.md +3 -3
  4. package/bin/cli.ts +4 -2
  5. package/dist/core/client.d.ts +6 -0
  6. package/dist/core/client.d.ts.map +1 -1
  7. package/dist/core/client.js +71 -0
  8. package/dist/lib.d.ts +2 -0
  9. package/dist/lib.d.ts.map +1 -1
  10. package/dist/lib.js +1 -0
  11. package/dist/operations/advanced-orders.test.d.ts +2 -0
  12. package/dist/operations/advanced-orders.test.d.ts.map +1 -0
  13. package/dist/operations/advanced-orders.test.js +189 -0
  14. package/dist/operations/bracket.d.ts +22 -0
  15. package/dist/operations/bracket.d.ts.map +1 -1
  16. package/dist/operations/bracket.js +106 -73
  17. package/dist/operations/chase.d.ts +19 -0
  18. package/dist/operations/chase.d.ts.map +1 -1
  19. package/dist/operations/chase.js +125 -58
  20. package/dist/operations/execution.d.ts +53 -0
  21. package/dist/operations/execution.d.ts.map +1 -0
  22. package/dist/operations/execution.js +106 -0
  23. package/dist/operations/scale.d.ts +50 -1
  24. package/dist/operations/scale.d.ts.map +1 -1
  25. package/dist/operations/scale.js +143 -105
  26. package/dist/setup/install.js +100 -3
  27. package/dist/setup/package-catalog.d.ts +12 -0
  28. package/dist/setup/package-catalog.d.ts.map +1 -0
  29. package/dist/setup/package-catalog.js +36 -0
  30. package/dist/setup/package-catalog.test.d.ts +2 -0
  31. package/dist/setup/package-catalog.test.d.ts.map +1 -0
  32. package/dist/setup/package-catalog.test.js +31 -0
  33. package/package.json +5 -3
  34. package/scripts/core/client.ts +91 -0
  35. package/scripts/lib.ts +3 -0
  36. package/scripts/operations/advanced-orders.test.ts +209 -0
  37. package/scripts/operations/bracket.ts +128 -72
  38. package/scripts/operations/chase.ts +138 -61
  39. package/scripts/operations/execution.ts +138 -0
  40. package/scripts/operations/scale.ts +186 -131
  41. package/scripts/setup/install.ts +110 -3
  42. package/scripts/setup/package-catalog.test.ts +50 -0
  43. package/scripts/setup/package-catalog.ts +49 -0
@@ -3,6 +3,7 @@
3
3
  import { fileURLToPath } from 'url';
4
4
  import { getClient } from '../core/client.js';
5
5
  import { formatUsd, parseArgs, sleep } from '../core/utils.js';
6
+ import { UserFillWatcher } from './execution.js';
6
7
  function printUsage() {
7
8
  console.log(`
8
9
  Open Broker - Bracket Order
@@ -23,6 +24,8 @@ Options:
23
24
  --tp Take profit distance in % from entry
24
25
  --sl Stop loss distance in % from entry
25
26
  --slippage Slippage for market entry in bps (default: 50)
27
+ --entry-timeout Seconds to wait for limit entry fill before returning (default: 300)
28
+ --sl-slippage Stop-loss trigger limit slippage in bps (default: 100)
26
29
  --leverage Set leverage (e.g., 10 for 10x). Cross for main perps, isolated for HIP-3
27
30
  --dry Dry run - show bracket plan without executing
28
31
 
@@ -52,7 +55,7 @@ export async function runBracket(opts) {
52
55
  if (entryType === 'limit' && opts.entryPrice === undefined) {
53
56
  throw new Error('entryPrice is required for limit entry');
54
57
  }
55
- const client = getClient();
58
+ const client = opts.client ?? getClient();
56
59
  if (opts.verbose)
57
60
  client.verbose = true;
58
61
  out('Open Broker - Bracket Order');
@@ -97,52 +100,91 @@ export async function runBracket(opts) {
97
100
  out('Step 1: Entry order');
98
101
  let actualEntry = entry;
99
102
  let entryOid = null;
100
- if (entryType === 'market') {
101
- const entryResponse = await client.marketOrder(opts.coin, isLong, opts.size, opts.slippage, opts.leverage);
102
- if (entryResponse.status === 'ok' && entryResponse.response && typeof entryResponse.response === 'object') {
103
- const status = entryResponse.response.data.statuses[0];
104
- if (status?.filled) {
105
- actualEntry = parseFloat(status.filled.avgPx);
106
- out(` ✅ Filled @ ${formatUsd(actualEntry)}`);
103
+ let filledSize = 0;
104
+ const ownsFillWatcher = !opts.fillWatcher;
105
+ const fillWatcher = opts.fillWatcher ?? new UserFillWatcher(client, { sinceMs: Date.now() });
106
+ await fillWatcher.start();
107
+ try {
108
+ if (entryType === 'market') {
109
+ const entryResponse = await client.marketOrder(opts.coin, isLong, opts.size, opts.slippage, opts.leverage);
110
+ if (entryResponse.status === 'ok' && entryResponse.response && typeof entryResponse.response === 'object') {
111
+ const status = entryResponse.response.data.statuses[0];
112
+ if (status?.filled) {
113
+ actualEntry = parseFloat(status.filled.avgPx);
114
+ filledSize = parseFloat(status.filled.totalSz);
115
+ out(` ✅ Filled ${filledSize} @ ${formatUsd(actualEntry)}`);
116
+ }
117
+ else if (status?.error) {
118
+ out(` ❌ Entry failed: ${status.error}`);
119
+ out('\n⚠️ Bracket aborted - no position opened');
120
+ return { status: 'entry_failed', reason: status.error };
121
+ }
122
+ else {
123
+ out(` ❌ Entry failed: unexpected response`);
124
+ out('\n⚠️ Bracket aborted - no confirmed position opened');
125
+ return { status: 'entry_failed', reason: 'Unexpected entry response' };
126
+ }
107
127
  }
108
- else if (status?.error) {
109
- out(` ❌ Entry failed: ${status.error}`);
128
+ else {
129
+ const reason = typeof entryResponse.response === 'string' ? entryResponse.response : 'Unknown error';
130
+ out(` ❌ Entry failed: ${reason}`);
110
131
  out('\n⚠️ Bracket aborted - no position opened');
111
- return { status: 'entry_failed', reason: status.error };
132
+ return { status: 'entry_failed', reason };
112
133
  }
113
134
  }
114
135
  else {
115
- const reason = typeof entryResponse.response === 'string' ? entryResponse.response : 'Unknown error';
116
- out(` ❌ Entry failed: ${reason}`);
117
- out('\n⚠️ Bracket aborted - no position opened');
118
- return { status: 'entry_failed', reason };
119
- }
120
- }
121
- else {
122
- const entryResponse = await client.limitOrder(opts.coin, isLong, opts.size, entry, 'Gtc', false, opts.leverage);
123
- if (entryResponse.status === 'ok' && entryResponse.response && typeof entryResponse.response === 'object') {
124
- const status = entryResponse.response.data.statuses[0];
125
- if (status?.resting) {
126
- entryOid = status.resting.oid;
127
- out(` ✅ Limit order placed @ ${formatUsd(entry)} (OID: ${entryOid})`);
128
- out(` ⏳ Waiting for fill before placing TP/SL...`);
129
- out('\n⚠️ Note: TP/SL will be placed after entry fills. Monitor manually or use a strategy script.');
130
- return { status: 'limit_resting', entryOid, entryPrice: entry };
136
+ const entryResponse = await client.limitOrder(opts.coin, isLong, opts.size, entry, 'Gtc', false, opts.leverage);
137
+ if (entryResponse.status === 'ok' && entryResponse.response && typeof entryResponse.response === 'object') {
138
+ const status = entryResponse.response.data.statuses[0];
139
+ if (status?.resting) {
140
+ entryOid = status.resting.oid;
141
+ const entryTimeoutSec = opts.entryTimeoutSec ?? 300;
142
+ out(` ✅ Limit order placed @ ${formatUsd(entry)} (OID: ${entryOid})`);
143
+ if (entryTimeoutSec <= 0) {
144
+ out(` ⏳ Entry resting; TP/SL not armed until a fill is confirmed.`);
145
+ return { status: 'limit_resting', entryOid, entryPrice: entry };
146
+ }
147
+ out(` ⏳ Waiting up to ${entryTimeoutSec}s for fill confirmation...`);
148
+ const fill = await fillWatcher.waitForFill(entryOid, opts.size, entryTimeoutSec * 1000, { coin: opts.coin });
149
+ if (fill.size <= 0) {
150
+ out(` ⚠️ Entry still resting after ${entryTimeoutSec}s; TP/SL not armed.`);
151
+ return { status: 'limit_resting', entryOid, entryPrice: entry };
152
+ }
153
+ filledSize = Math.min(fill.size, opts.size);
154
+ actualEntry = fill.avgPrice ?? entry;
155
+ out(` ✅ Fill confirmed: ${filledSize} @ ${formatUsd(actualEntry)}`);
156
+ if (filledSize < opts.size * 0.999) {
157
+ out(` ⚠️ Partial entry fill; arming TP/SL for filled size only.`);
158
+ }
159
+ }
160
+ else if (status?.filled) {
161
+ actualEntry = parseFloat(status.filled.avgPx);
162
+ filledSize = parseFloat(status.filled.totalSz);
163
+ out(` ✅ Filled immediately ${filledSize} @ ${formatUsd(actualEntry)}`);
164
+ }
165
+ else if (status?.error) {
166
+ out(` ❌ Entry failed: ${status.error}`);
167
+ return { status: 'entry_failed', reason: status.error };
168
+ }
169
+ else {
170
+ out(` ❌ Entry failed: unexpected response`);
171
+ return { status: 'entry_failed', reason: 'Unexpected entry response' };
172
+ }
131
173
  }
132
- else if (status?.filled) {
133
- actualEntry = parseFloat(status.filled.avgPx);
134
- out(` ✅ Filled immediately @ ${formatUsd(actualEntry)}`);
174
+ else {
175
+ out(` ❌ Entry failed`);
176
+ return { status: 'entry_failed', reason: 'Unknown error' };
135
177
  }
136
- else if (status?.error) {
137
- out(` ❌ Entry failed: ${status.error}`);
138
- return { status: 'entry_failed', reason: status.error };
139
- }
140
- }
141
- else {
142
- out(` ❌ Entry failed`);
143
- return { status: 'entry_failed', reason: 'Unknown error' };
144
178
  }
145
179
  }
180
+ finally {
181
+ if (ownsFillWatcher)
182
+ await fillWatcher.stop();
183
+ }
184
+ if (!Number.isFinite(filledSize) || filledSize <= 0) {
185
+ out('\n⚠️ Bracket aborted - no confirmed fill size');
186
+ return { status: 'entry_failed', reason: 'No confirmed fill size' };
187
+ }
146
188
  // Recalculate TP/SL based on actual entry
147
189
  if (isLong) {
148
190
  tpPrice = actualEntry * (1 + opts.tpPct / 100);
@@ -153,60 +195,46 @@ export async function runBracket(opts) {
153
195
  slPrice = actualEntry * (1 + opts.slPct / 100);
154
196
  }
155
197
  await sleep(500);
156
- // Step 2: Take Profit (trigger order)
157
- out('\nStep 2: Take Profit order (trigger)');
158
- const tpSide = !isLong;
159
- const tpResponse = await client.takeProfit(opts.coin, tpSide, opts.size, tpPrice);
198
+ // Step 2: Paired TP/SL trigger orders
199
+ out('\nStep 2: Paired TP/SL trigger orders');
200
+ const exitSide = !isLong;
201
+ const pairResponse = await client.tpslPair(opts.coin, exitSide, filledSize, tpPrice, slPrice, opts.slSlippageBps, opts.leverage);
160
202
  let tpOid = null;
161
- if (tpResponse.status === 'ok' && tpResponse.response && typeof tpResponse.response === 'object') {
162
- const status = tpResponse.response.data.statuses[0];
163
- if (status?.resting) {
164
- tpOid = status.resting.oid;
203
+ let slOid = null;
204
+ if (pairResponse.status === 'ok' && pairResponse.response && typeof pairResponse.response === 'object') {
205
+ const [tpStatus, slStatus] = pairResponse.response.data.statuses;
206
+ if (tpStatus?.resting) {
207
+ tpOid = tpStatus.resting.oid;
165
208
  out(` ✅ TP trigger placed @ ${formatUsd(tpPrice)} (OID: ${tpOid})`);
166
209
  }
167
- else if (status?.error) {
168
- out(` ❌ TP failed: ${status.error}`);
210
+ else if (tpStatus?.error) {
211
+ out(` ❌ TP failed: ${tpStatus.error}`);
169
212
  }
170
213
  else {
171
- out(` ⚠️ TP status: ${JSON.stringify(status)}`);
214
+ out(` ⚠️ TP status: ${JSON.stringify(tpStatus)}`);
172
215
  }
173
- }
174
- else {
175
- const reason = typeof tpResponse.response === 'string' ? tpResponse.response : 'Unknown error';
176
- out(` ❌ TP failed: ${reason}`);
177
- }
178
- await sleep(500);
179
- // Step 3: Stop Loss (trigger order)
180
- out('\nStep 3: Stop Loss order (trigger)');
181
- const slSide = !isLong;
182
- const slResponse = await client.stopLoss(opts.coin, slSide, opts.size, slPrice);
183
- let slOid = null;
184
- if (slResponse.status === 'ok' && slResponse.response && typeof slResponse.response === 'object') {
185
- const status = slResponse.response.data.statuses[0];
186
- if (status?.resting) {
187
- slOid = status.resting.oid;
216
+ if (slStatus?.resting) {
217
+ slOid = slStatus.resting.oid;
188
218
  out(` ✅ SL trigger placed @ ${formatUsd(slPrice)} (OID: ${slOid})`);
189
219
  }
190
- else if (status?.error) {
191
- out(` ❌ SL failed: ${status.error}`);
220
+ else if (slStatus?.error) {
221
+ out(` ❌ SL failed: ${slStatus.error}`);
192
222
  }
193
223
  else {
194
- out(` ⚠️ SL status: ${JSON.stringify(status)}`);
224
+ out(` ⚠️ SL status: ${JSON.stringify(slStatus)}`);
195
225
  }
196
226
  }
197
227
  else {
198
- const reason = typeof slResponse.response === 'string' ? slResponse.response : 'Unknown error';
199
- out(` ❌ SL failed: ${reason}`);
228
+ const reason = typeof pairResponse.response === 'string' ? pairResponse.response : 'Unknown error';
229
+ out(` ❌ TP/SL pair failed: ${reason}`);
200
230
  }
201
231
  out('\n========== Bracket Summary ==========');
202
- out(`Position: ${isLong ? 'LONG' : 'SHORT'} ${opts.size} ${opts.coin}`);
232
+ out(`Position: ${isLong ? 'LONG' : 'SHORT'} ${filledSize} ${opts.coin}`);
203
233
  out(`Entry: ${formatUsd(actualEntry)}`);
204
234
  out(`Take Profit: ${formatUsd(tpPrice)} (+${opts.tpPct}%) - Trigger order`);
205
235
  out(`Stop Loss: ${formatUsd(slPrice)} (-${opts.slPct}%) - Trigger order`);
206
236
  if (tpOid && slOid) {
207
- out(`\n✅ Bracket complete! TP and SL are trigger orders.`);
208
- out(` They will only execute when price reaches trigger level.`);
209
- out(` When one fills, cancel the other manually.`);
237
+ out(`\n✅ Bracket complete! TP and SL are linked trigger orders.`);
210
238
  }
211
239
  return {
212
240
  status: tpOid && slOid ? 'complete' : 'partial',
@@ -215,6 +243,7 @@ export async function runBracket(opts) {
215
243
  slPrice,
216
244
  tpOid,
217
245
  slOid,
246
+ protectedSize: filledSize,
218
247
  };
219
248
  }
220
249
  async function main() {
@@ -227,6 +256,8 @@ async function main() {
227
256
  const tpPct = parseFloat(args.tp);
228
257
  const slPct = parseFloat(args.sl);
229
258
  const slippage = args.slippage ? parseInt(args.slippage) : undefined;
259
+ const entryTimeoutSec = args['entry-timeout'] ? parseInt(args['entry-timeout']) : undefined;
260
+ const slSlippageBps = args['sl-slippage'] ? parseInt(args['sl-slippage']) : undefined;
230
261
  const leverage = args.leverage ? parseInt(args.leverage) : undefined;
231
262
  const dryRun = args.dry;
232
263
  if (!coin || !side || isNaN(size) || isNaN(tpPct) || isNaN(slPct)) {
@@ -247,6 +278,8 @@ async function main() {
247
278
  entryType,
248
279
  entryPrice,
249
280
  slippage,
281
+ entryTimeoutSec,
282
+ slSlippageBps,
250
283
  leverage,
251
284
  dryRun,
252
285
  verbose: args.verbose,
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env npx tsx
2
+ import type { OrderResponse, CancelResponse, OpenOrder } from '../core/types.js';
3
+ import { type FillWatcher } from './execution.js';
2
4
  export interface ChaseOptions {
3
5
  coin: string;
4
6
  side: 'buy' | 'sell';
@@ -11,9 +13,26 @@ export interface ChaseOptions {
11
13
  reduceOnly?: boolean;
12
14
  dryRun?: boolean;
13
15
  verbose?: boolean;
16
+ client?: ChaseClient;
17
+ fillWatcher?: FillWatcher;
14
18
  /** Receives each output line. Defaults to console.log. */
15
19
  output?: (line: string) => void;
16
20
  }
21
+ export interface ChaseClient {
22
+ verbose: boolean;
23
+ address: string;
24
+ getAllMids(): Promise<Record<string, string>>;
25
+ getOpenOrders(): Promise<OpenOrder[]>;
26
+ getUserFills(user?: string): Promise<Array<{
27
+ coin: string;
28
+ px: string;
29
+ sz: string;
30
+ time: number;
31
+ oid: number;
32
+ }>>;
33
+ limitOrder(coin: string, isBuy: boolean, size: number, price: number, tif?: 'Gtc' | 'Ioc' | 'Alo', reduceOnly?: boolean, leverage?: number): Promise<OrderResponse>;
34
+ cancel(coin: string, oid: number): Promise<CancelResponse>;
35
+ }
17
36
  export interface ChaseResult {
18
37
  status: 'dry' | 'filled' | 'timeout' | 'max_chase_exceeded';
19
38
  iterations: number;
@@ -1 +1 @@
1
- {"version":3,"file":"chase.d.ts","sourceRoot":"","sources":["../../scripts/operations/chase.ts"],"names":[],"mappings":";AAuCA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,SAAS,GAAG,oBAAoB,CAAC;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA2JvE"}
1
+ {"version":3,"file":"chase.d.ts","sourceRoot":"","sources":["../../scripts/operations/chase.ts"],"names":[],"mappings":";AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAEjF,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAkCnE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,aAAa,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACtC,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACjH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,EAAE,UAAU,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACpK,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,SAAS,GAAG,oBAAoB,CAAC;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA0NvE"}
@@ -3,6 +3,7 @@
3
3
  import { fileURLToPath } from 'url';
4
4
  import { getClient } from '../core/client.js';
5
5
  import { formatUsd, parseArgs, sleep } from '../core/utils.js';
6
+ import { UserFillWatcher } from './execution.js';
6
7
  function printUsage() {
7
8
  console.log(`
8
9
  Open Broker - Chase Order
@@ -43,7 +44,15 @@ export async function runChase(opts) {
43
44
  const isBuy = opts.side === 'buy';
44
45
  if (opts.size <= 0 || isNaN(opts.size))
45
46
  throw new Error('size must be positive');
46
- const client = getClient();
47
+ if (offsetBps < 0 || !Number.isFinite(offsetBps))
48
+ throw new Error('offsetBps must be non-negative');
49
+ if (timeoutSec <= 0 || !Number.isFinite(timeoutSec))
50
+ throw new Error('timeoutSec must be positive');
51
+ if (intervalMs <= 0 || !Number.isFinite(intervalMs))
52
+ throw new Error('intervalMs must be positive');
53
+ if (maxChaseBps <= 0 || !Number.isFinite(maxChaseBps))
54
+ throw new Error('maxChaseBps must be positive');
55
+ const client = opts.client ?? getClient();
47
56
  if (opts.verbose)
48
57
  client.verbose = true;
49
58
  out('Open Broker - Chase Order');
@@ -74,87 +83,139 @@ export async function runChase(opts) {
74
83
  const startTime = Date.now();
75
84
  let currentOid = null;
76
85
  let lastPrice = null;
86
+ let remainingSize = opts.size;
77
87
  let iteration = 0;
78
88
  let filled = false;
79
89
  let exitReason = 'timeout';
80
- while (Date.now() - startTime < timeoutSec * 1000) {
81
- iteration++;
82
- const currentMids = await client.getAllMids();
83
- const currentMid = parseFloat(currentMids[opts.coin]);
84
- if (isBuy && currentMid > maxChasePrice) {
85
- out(`\n⚠️ Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
86
- exitReason = 'max_chase_exceeded';
87
- break;
90
+ const accountedFills = new Map();
91
+ const ownsFillWatcher = !opts.fillWatcher;
92
+ const fillWatcher = opts.fillWatcher ?? new UserFillWatcher(client, { sinceMs: startTime });
93
+ const applyFills = (oid) => {
94
+ const totalFilled = fillWatcher.getFilled(oid).size;
95
+ const alreadyAccounted = accountedFills.get(oid) ?? 0;
96
+ const delta = Math.max(0, totalFilled - alreadyAccounted);
97
+ if (delta > 0) {
98
+ remainingSize = Math.max(0, remainingSize - delta);
99
+ accountedFills.set(oid, totalFilled);
88
100
  }
89
- if (!isBuy && currentMid < maxChasePrice) {
90
- out(`\n⚠️ Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
91
- exitReason = 'max_chase_exceeded';
92
- break;
93
- }
94
- const orderPrice = isBuy
95
- ? currentMid * (1 - offsetBps / 10000)
96
- : currentMid * (1 + offsetBps / 10000);
97
- const priceChanged = !lastPrice || Math.abs(orderPrice - lastPrice) / lastPrice > 0.0001;
98
- if (priceChanged) {
101
+ return delta;
102
+ };
103
+ await fillWatcher.start();
104
+ try {
105
+ while (Date.now() - startTime < timeoutSec * 1000) {
106
+ iteration++;
99
107
  if (currentOid !== null) {
100
- try {
101
- await client.cancel(opts.coin, currentOid);
102
- }
103
- catch {
104
- // Order might have filled
105
- }
106
- currentOid = null;
107
- }
108
- const orders = await client.getOpenOrders();
109
- const ourOrder = orders.find(o => o.coin === opts.coin && o.oid === currentOid);
110
- if (!ourOrder && currentOid !== null) {
111
- const state = await client.getUserState();
112
- const pos = state.assetPositions.find(p => p.position.coin === opts.coin);
113
- if (pos && Math.abs(parseFloat(pos.position.szi)) >= opts.size * 0.99) {
108
+ applyFills(currentOid);
109
+ if (remainingSize <= opts.size * 0.001) {
114
110
  filled = true;
115
111
  exitReason = 'filled';
116
112
  out(`\n✅ Order filled!`);
117
113
  break;
118
114
  }
119
115
  }
120
- out(`[${iteration}] Mid: ${formatUsd(currentMid)} Order: ${formatUsd(orderPrice)}...`);
121
- const response = await client.limitOrder(opts.coin, isBuy, opts.size, orderPrice, 'Alo', opts.reduceOnly, opts.leverage);
122
- if (response.status === 'ok' && response.response && typeof response.response === 'object') {
123
- const status = response.response.data.statuses[0];
124
- if (status?.resting) {
125
- currentOid = status.resting.oid;
126
- lastPrice = orderPrice;
127
- out(`OID: ${currentOid}`);
116
+ const currentMids = await client.getAllMids();
117
+ const currentMid = parseFloat(currentMids[opts.coin]);
118
+ if (!currentMid)
119
+ throw new Error(`No market data for ${opts.coin}`);
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
+ }
130
+ const orderPrice = isBuy
131
+ ? currentMid * (1 - offsetBps / 10000)
132
+ : currentMid * (1 + offsetBps / 10000);
133
+ const priceChanged = !lastPrice || Math.abs(orderPrice - lastPrice) / lastPrice > 0.0001;
134
+ if (priceChanged) {
135
+ if (currentOid !== null) {
136
+ applyFills(currentOid);
137
+ if (remainingSize <= opts.size * 0.001) {
138
+ filled = true;
139
+ exitReason = 'filled';
140
+ out(`\n✅ Order filled!`);
141
+ break;
142
+ }
143
+ try {
144
+ await client.cancel(opts.coin, currentOid);
145
+ }
146
+ catch {
147
+ // Order might have filled between the fill check and cancel.
148
+ }
149
+ applyFills(currentOid);
150
+ currentOid = null;
128
151
  }
129
- else if (status?.filled) {
152
+ if (remainingSize <= opts.size * 0.001) {
130
153
  filled = true;
131
154
  exitReason = 'filled';
132
- out(`✅ Filled @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
155
+ out(`\n✅ Order filled!`);
133
156
  break;
134
157
  }
135
- else if (status?.error) {
136
- out(`❌ ${status.error}`);
158
+ out(`[${iteration}] Mid: ${formatUsd(currentMid)} → Order: ${formatUsd(orderPrice)} x ${remainingSize.toFixed(6)}...`);
159
+ const response = await client.limitOrder(opts.coin, isBuy, remainingSize, orderPrice, 'Alo', opts.reduceOnly, opts.leverage);
160
+ if (response.status === 'ok' && response.response && typeof response.response === 'object') {
161
+ const status = response.response.data.statuses[0];
162
+ if (status?.resting) {
163
+ currentOid = status.resting.oid;
164
+ lastPrice = orderPrice;
165
+ out(`OID: ${currentOid}`);
166
+ }
167
+ else if (status?.filled) {
168
+ const totalSz = parseFloat(status.filled.totalSz);
169
+ remainingSize = Math.max(0, remainingSize - totalSz);
170
+ out(`✅ Filled ${totalSz} @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
171
+ if (remainingSize <= opts.size * 0.001) {
172
+ filled = true;
173
+ exitReason = 'filled';
174
+ break;
175
+ }
176
+ }
177
+ else if (status?.error) {
178
+ out(`❌ ${status.error}`);
179
+ }
180
+ }
181
+ else {
182
+ out(`❌ Failed`);
137
183
  }
138
184
  }
139
185
  else {
140
- out(`❌ Failed`);
141
- }
142
- }
143
- else {
144
- if (currentOid !== null) {
145
- const orders = await client.getOpenOrders();
146
- const ourOrder = orders.find(o => o.oid === currentOid);
147
- if (!ourOrder) {
148
- filled = true;
149
- exitReason = 'filled';
150
- out(`\n✅ Order filled!`);
151
- break;
186
+ if (currentOid !== null) {
187
+ await fillWatcher.waitForFill(currentOid, remainingSize, intervalMs, { coin: opts.coin, pollMs: intervalMs });
188
+ applyFills(currentOid);
189
+ if (remainingSize <= opts.size * 0.001) {
190
+ filled = true;
191
+ exitReason = 'filled';
192
+ out(`\n✅ Order filled!`);
193
+ break;
194
+ }
195
+ const orders = await client.getOpenOrders();
196
+ const ourOrder = orders.find(o => o.oid === currentOid);
197
+ if (!ourOrder) {
198
+ applyFills(currentOid);
199
+ if (remainingSize <= opts.size * 0.001) {
200
+ filled = true;
201
+ exitReason = 'filled';
202
+ out(`\n✅ Order filled!`);
203
+ break;
204
+ }
205
+ currentOid = null;
206
+ lastPrice = null;
207
+ }
152
208
  }
153
209
  }
210
+ await sleep(intervalMs);
154
211
  }
155
- await sleep(intervalMs);
212
+ }
213
+ finally {
214
+ if (ownsFillWatcher)
215
+ await fillWatcher.stop();
156
216
  }
157
217
  if (currentOid !== null && !filled) {
218
+ applyFills(currentOid);
158
219
  out(`\nCancelling unfilled order...`);
159
220
  try {
160
221
  await client.cancel(opts.coin, currentOid);
@@ -163,6 +224,11 @@ export async function runChase(opts) {
163
224
  catch {
164
225
  out(`⚠️ Could not cancel (may have filled)`);
165
226
  }
227
+ applyFills(currentOid);
228
+ if (remainingSize <= opts.size * 0.001) {
229
+ filled = true;
230
+ exitReason = 'filled';
231
+ }
166
232
  }
167
233
  const elapsed = (Date.now() - startTime) / 1000;
168
234
  const endMid = parseFloat((await client.getAllMids())[opts.coin]);
@@ -172,6 +238,7 @@ export async function runChase(opts) {
172
238
  out(`Iterations: ${iteration}`);
173
239
  out(`Start Mid: ${formatUsd(startMid)}`);
174
240
  out(`End Mid: ${formatUsd(endMid)} (${priceMove >= 0 ? '+' : ''}${priceMove.toFixed(1)} bps)`);
241
+ out(`Filled Size: ${(opts.size - remainingSize).toFixed(6)} / ${opts.size}`);
175
242
  out(`Result: ${filled ? '✅ Filled' : '❌ Not filled'}`);
176
243
  return { status: exitReason, iterations: iteration, durationSec: elapsed, startMid, endMid };
177
244
  }
@@ -0,0 +1,53 @@
1
+ import type { HyperliquidClient } from '../core/client.js';
2
+ import { WebSocketManager } from '../core/ws.js';
3
+ export interface FillSummary {
4
+ size: number;
5
+ notional: number;
6
+ avgPrice?: number;
7
+ }
8
+ export interface FillWatcher {
9
+ start(): Promise<void>;
10
+ stop(): Promise<void>;
11
+ getFilled(oid: number): FillSummary;
12
+ waitForFill(oid: number, targetSize: number, timeoutMs: number, options?: {
13
+ coin?: string;
14
+ pollMs?: number;
15
+ }): Promise<FillSummary>;
16
+ }
17
+ type RestFill = {
18
+ coin: string;
19
+ px: string;
20
+ sz: string;
21
+ time: number;
22
+ oid: number;
23
+ };
24
+ type FillClient = Pick<HyperliquidClient, 'address' | 'verbose'> & {
25
+ getUserFills(user?: string): Promise<RestFill[]>;
26
+ };
27
+ export declare class UserFillWatcher implements FillWatcher {
28
+ private readonly client;
29
+ private fills;
30
+ private seenFills;
31
+ private ws;
32
+ private ownsWs;
33
+ private started;
34
+ private readonly sinceMs;
35
+ private readonly user;
36
+ private readonly onFill;
37
+ constructor(client: FillClient, options?: {
38
+ ws?: WebSocketManager | null;
39
+ sinceMs?: number;
40
+ user?: string;
41
+ });
42
+ start(): Promise<void>;
43
+ stop(): Promise<void>;
44
+ getFilled(oid: number): FillSummary;
45
+ waitForFill(oid: number, targetSize: number, timeoutMs: number, options?: {
46
+ coin?: string;
47
+ pollMs?: number;
48
+ }): Promise<FillSummary>;
49
+ private refreshRestFills;
50
+ private recordFill;
51
+ }
52
+ export {};
53
+ //# sourceMappingURL=execution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execution.d.ts","sourceRoot":"","sources":["../../scripts/operations/execution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAmB,MAAM,eAAe,CAAC;AAGlE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;IACpC,WAAW,CACT,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3C,OAAO,CAAC,WAAW,CAAC,CAAC;CACzB;AAED,KAAK,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpF,KAAK,UAAU,GAAG,IAAI,CAAC,iBAAiB,EAAE,SAAS,GAAG,SAAS,CAAC,GAAG;IACjE,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;CAClD,CAAC;AAEF,qBAAa,eAAgB,YAAW,WAAW;IAmB/C,OAAO,CAAC,QAAQ,CAAC,MAAM;IAlBzB,OAAO,CAAC,KAAK,CAAkC;IAC/C,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgB;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAQrB;gBAGiB,MAAM,EAAE,UAAU,EACnC,OAAO,GAAE;QAAE,EAAE,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO;IAQ3E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAetB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW;IAI7B,WAAW,CACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAC/C,OAAO,CAAC,WAAW,CAAC;YAeT,gBAAgB;IAW9B,OAAO,CAAC,UAAU;CAuBnB"}