openbroker 1.9.5 → 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.
- package/README.md +6 -4
- package/SKILL.md +2 -2
- package/dist/core/client.d.ts +6 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +71 -0
- package/dist/lib.d.ts +2 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/dist/operations/advanced-orders.test.d.ts +2 -0
- package/dist/operations/advanced-orders.test.d.ts.map +1 -0
- package/dist/operations/advanced-orders.test.js +189 -0
- package/dist/operations/bracket.d.ts +22 -0
- package/dist/operations/bracket.d.ts.map +1 -1
- package/dist/operations/bracket.js +106 -73
- package/dist/operations/chase.d.ts +19 -0
- package/dist/operations/chase.d.ts.map +1 -1
- package/dist/operations/chase.js +125 -58
- package/dist/operations/execution.d.ts +53 -0
- package/dist/operations/execution.d.ts.map +1 -0
- package/dist/operations/execution.js +106 -0
- package/dist/operations/scale.d.ts +50 -1
- package/dist/operations/scale.d.ts.map +1 -1
- package/dist/operations/scale.js +143 -105
- package/package.json +3 -2
- package/scripts/core/client.ts +91 -0
- package/scripts/lib.ts +3 -0
- package/scripts/operations/advanced-orders.test.ts +209 -0
- package/scripts/operations/bracket.ts +128 -72
- package/scripts/operations/chase.ts +138 -61
- package/scripts/operations/execution.ts +138 -0
- package/scripts/operations/scale.ts +186 -131
|
@@ -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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
109
|
-
|
|
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
|
|
132
|
+
return { status: 'entry_failed', reason };
|
|
112
133
|
}
|
|
113
134
|
}
|
|
114
135
|
else {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
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:
|
|
157
|
-
out('\nStep 2:
|
|
158
|
-
const
|
|
159
|
-
const
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 (
|
|
168
|
-
out(` ❌ TP failed: ${
|
|
210
|
+
else if (tpStatus?.error) {
|
|
211
|
+
out(` ❌ TP failed: ${tpStatus.error}`);
|
|
169
212
|
}
|
|
170
213
|
else {
|
|
171
|
-
out(` ⚠️ TP status: ${JSON.stringify(
|
|
214
|
+
out(` ⚠️ TP status: ${JSON.stringify(tpStatus)}`);
|
|
172
215
|
}
|
|
173
|
-
|
|
174
|
-
|
|
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 (
|
|
191
|
-
out(` ❌ SL failed: ${
|
|
220
|
+
else if (slStatus?.error) {
|
|
221
|
+
out(` ❌ SL failed: ${slStatus.error}`);
|
|
192
222
|
}
|
|
193
223
|
else {
|
|
194
|
-
out(` ⚠️ SL status: ${JSON.stringify(
|
|
224
|
+
out(` ⚠️ SL status: ${JSON.stringify(slStatus)}`);
|
|
195
225
|
}
|
|
196
226
|
}
|
|
197
227
|
else {
|
|
198
|
-
const reason = typeof
|
|
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'} ${
|
|
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":";
|
|
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"}
|
package/dist/operations/chase.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
152
|
+
if (remainingSize <= opts.size * 0.001) {
|
|
130
153
|
filled = true;
|
|
131
154
|
exitReason = 'filled';
|
|
132
|
-
out(
|
|
155
|
+
out(`\n✅ Order filled!`);
|
|
133
156
|
break;
|
|
134
157
|
}
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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"}
|