openbroker 1.9.5 → 1.10.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.
- package/CHANGELOG.md +19 -0
- package/README.md +6 -4
- package/SKILL.md +14 -5
- package/dist/core/client.d.ts +63 -3
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +246 -11
- package/dist/core/utils.d.ts +29 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +27 -0
- package/dist/lib.d.ts +4 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +2 -1
- 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 +478 -0
- package/dist/operations/bracket.d.ts +55 -3
- package/dist/operations/bracket.d.ts.map +1 -1
- package/dist/operations/bracket.js +324 -117
- package/dist/operations/chase.d.ts +20 -1
- package/dist/operations/chase.d.ts.map +1 -1
- package/dist/operations/chase.js +169 -67
- 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 +152 -105
- package/dist/operations/set-tpsl.js +69 -57
- package/dist/operations/trigger-order.js +18 -6
- package/dist/operations/twap.js +13 -1
- package/package.json +3 -2
- package/scripts/core/client.ts +320 -10
- package/scripts/core/utils.ts +37 -0
- package/scripts/lib.ts +6 -0
- package/scripts/operations/advanced-orders.test.ts +542 -0
- package/scripts/operations/bracket.ts +350 -115
- package/scripts/operations/chase.ts +183 -73
- package/scripts/operations/execution.ts +138 -0
- package/scripts/operations/scale.ts +195 -131
- package/scripts/operations/set-tpsl.ts +62 -57
- package/scripts/operations/trigger-order.ts +20 -6
- package/scripts/operations/twap.ts +14 -1
package/dist/operations/chase.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Chase Order - Follow price with limit orders until filled
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { getClient } from '../core/client.js';
|
|
5
|
-
import { formatUsd, parseArgs, sleep } from '../core/utils.js';
|
|
5
|
+
import { MIN_ORDER_NOTIONAL_USD, 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');
|
|
@@ -52,6 +61,10 @@ export async function runChase(opts) {
|
|
|
52
61
|
const startMid = parseFloat(mids[opts.coin]);
|
|
53
62
|
if (!startMid)
|
|
54
63
|
throw new Error(`No market data for ${opts.coin}`);
|
|
64
|
+
const startBelowMinimum = !opts.reduceOnly && opts.size * startMid < MIN_ORDER_NOTIONAL_USD;
|
|
65
|
+
const minNotionalMsg = `Chase size (~$${(opts.size * startMid).toFixed(2)}) is below the $${MIN_ORDER_NOTIONAL_USD} exchange minimum`;
|
|
66
|
+
if (startBelowMinimum && !opts.dryRun)
|
|
67
|
+
throw new Error(minNotionalMsg);
|
|
55
68
|
const maxChasePrice = isBuy
|
|
56
69
|
? startMid * (1 + maxChaseBps / 10000)
|
|
57
70
|
: startMid * (1 - maxChaseBps / 10000);
|
|
@@ -67,6 +80,8 @@ export async function runChase(opts) {
|
|
|
67
80
|
out(`Update Rate: ${intervalMs}ms`);
|
|
68
81
|
out(`Order Type: ALO (post-only)`);
|
|
69
82
|
if (opts.dryRun) {
|
|
83
|
+
if (startBelowMinimum)
|
|
84
|
+
out(`\n⚠️ ${minNotionalMsg}`);
|
|
70
85
|
out('\n🔍 Dry run - chase not started');
|
|
71
86
|
return { status: 'dry', iterations: 0, durationSec: 0, startMid, endMid: startMid };
|
|
72
87
|
}
|
|
@@ -74,95 +89,181 @@ export async function runChase(opts) {
|
|
|
74
89
|
const startTime = Date.now();
|
|
75
90
|
let currentOid = null;
|
|
76
91
|
let lastPrice = null;
|
|
92
|
+
let remainingSize = opts.size;
|
|
77
93
|
let iteration = 0;
|
|
78
94
|
let filled = false;
|
|
79
95
|
let exitReason = 'timeout';
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
const accountedFills = new Map();
|
|
97
|
+
const ownsFillWatcher = !opts.fillWatcher;
|
|
98
|
+
const fillWatcher = opts.fillWatcher ?? new UserFillWatcher(client, { sinceMs: startTime });
|
|
99
|
+
const applyFills = (oid) => {
|
|
100
|
+
const totalFilled = fillWatcher.getFilled(oid).size;
|
|
101
|
+
const alreadyAccounted = accountedFills.get(oid) ?? 0;
|
|
102
|
+
const delta = Math.max(0, totalFilled - alreadyAccounted);
|
|
103
|
+
if (delta > 0) {
|
|
104
|
+
remainingSize = Math.max(0, remainingSize - delta);
|
|
105
|
+
accountedFills.set(oid, totalFilled);
|
|
88
106
|
}
|
|
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) {
|
|
107
|
+
return delta;
|
|
108
|
+
};
|
|
109
|
+
await fillWatcher.start();
|
|
110
|
+
try {
|
|
111
|
+
while (Date.now() - startTime < timeoutSec * 1000) {
|
|
112
|
+
iteration++;
|
|
99
113
|
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) {
|
|
114
|
+
applyFills(currentOid);
|
|
115
|
+
if (remainingSize <= opts.size * 0.001) {
|
|
114
116
|
filled = true;
|
|
115
117
|
exitReason = 'filled';
|
|
116
118
|
out(`\n✅ Order filled!`);
|
|
117
119
|
break;
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
// A transient /info failure mid-chase must not kill the execution (and
|
|
123
|
+
// strand the resting order) — wait a tick and retry instead.
|
|
124
|
+
let currentMid = NaN;
|
|
125
|
+
try {
|
|
126
|
+
const currentMids = await client.getAllMids();
|
|
127
|
+
currentMid = parseFloat(currentMids[opts.coin]);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* transient failure — retry next tick */
|
|
131
|
+
}
|
|
132
|
+
if (!Number.isFinite(currentMid) || currentMid <= 0) {
|
|
133
|
+
out(`[${iteration}] ⏳ No live price — retrying...`);
|
|
134
|
+
await sleep(intervalMs);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (isBuy && currentMid > maxChasePrice) {
|
|
138
|
+
out(`\n⚠️ Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
|
|
139
|
+
exitReason = 'max_chase_exceeded';
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
if (!isBuy && currentMid < maxChasePrice) {
|
|
143
|
+
out(`\n⚠️ Price ${formatUsd(currentMid)} exceeded max chase ${formatUsd(maxChasePrice)}`);
|
|
144
|
+
exitReason = 'max_chase_exceeded';
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
const orderPrice = isBuy
|
|
148
|
+
? currentMid * (1 - offsetBps / 10000)
|
|
149
|
+
: currentMid * (1 + offsetBps / 10000);
|
|
150
|
+
const priceChanged = !lastPrice || Math.abs(orderPrice - lastPrice) / lastPrice > 0.0001;
|
|
151
|
+
if (priceChanged) {
|
|
152
|
+
if (currentOid !== null) {
|
|
153
|
+
applyFills(currentOid);
|
|
154
|
+
if (remainingSize <= opts.size * 0.001) {
|
|
155
|
+
filled = true;
|
|
156
|
+
exitReason = 'filled';
|
|
157
|
+
out(`\n✅ Order filled!`);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await client.cancel(opts.coin, currentOid);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Order might have filled between the fill check and cancel.
|
|
165
|
+
}
|
|
166
|
+
applyFills(currentOid);
|
|
167
|
+
currentOid = null;
|
|
128
168
|
}
|
|
129
|
-
|
|
169
|
+
if (remainingSize <= opts.size * 0.001) {
|
|
130
170
|
filled = true;
|
|
131
171
|
exitReason = 'filled';
|
|
132
|
-
out(
|
|
172
|
+
out(`\n✅ Order filled!`);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
// The dust remainder of a partial fill can drop below the exchange
|
|
176
|
+
// minimum — placing it would just error out on every tick.
|
|
177
|
+
if (!opts.reduceOnly && remainingSize * currentMid < MIN_ORDER_NOTIONAL_USD) {
|
|
178
|
+
out(`\n⚠️ Remaining size (~$${(remainingSize * currentMid).toFixed(2)}) fell below the $${MIN_ORDER_NOTIONAL_USD} exchange minimum`);
|
|
179
|
+
exitReason = 'min_notional';
|
|
133
180
|
break;
|
|
134
181
|
}
|
|
135
|
-
|
|
136
|
-
|
|
182
|
+
out(`[${iteration}] Mid: ${formatUsd(currentMid)} → Order: ${formatUsd(orderPrice)} x ${remainingSize.toFixed(6)}...`);
|
|
183
|
+
const response = await client.limitOrder(opts.coin, isBuy, remainingSize, orderPrice, 'Alo', opts.reduceOnly, opts.leverage);
|
|
184
|
+
if (response.status === 'ok' && response.response && typeof response.response === 'object') {
|
|
185
|
+
const status = response.response.data.statuses[0];
|
|
186
|
+
if (status?.resting) {
|
|
187
|
+
currentOid = status.resting.oid;
|
|
188
|
+
lastPrice = orderPrice;
|
|
189
|
+
out(`OID: ${currentOid}`);
|
|
190
|
+
}
|
|
191
|
+
else if (status?.filled) {
|
|
192
|
+
const totalSz = parseFloat(status.filled.totalSz);
|
|
193
|
+
remainingSize = Math.max(0, remainingSize - totalSz);
|
|
194
|
+
out(`✅ Filled ${totalSz} @ ${formatUsd(parseFloat(status.filled.avgPx))}`);
|
|
195
|
+
if (remainingSize <= opts.size * 0.001) {
|
|
196
|
+
filled = true;
|
|
197
|
+
exitReason = 'filled';
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else if (status?.error) {
|
|
202
|
+
if (/post.?only|immediately match/i.test(status.error)) {
|
|
203
|
+
// Stale mid crossed the book — the fast-market condition chase
|
|
204
|
+
// exists for. Reprice on the next tick instead of dying.
|
|
205
|
+
out(`↩️ Quote would cross the book — repricing`);
|
|
206
|
+
lastPrice = null;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
throw new Error(status.error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const reason = typeof response.response === 'string' ? response.response : 'Order rejected';
|
|
215
|
+
throw new Error(reason);
|
|
137
216
|
}
|
|
138
217
|
}
|
|
139
218
|
else {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
219
|
+
if (currentOid !== null) {
|
|
220
|
+
await fillWatcher.waitForFill(currentOid, remainingSize, intervalMs, { coin: opts.coin, pollMs: intervalMs });
|
|
221
|
+
applyFills(currentOid);
|
|
222
|
+
if (remainingSize <= opts.size * 0.001) {
|
|
223
|
+
filled = true;
|
|
224
|
+
exitReason = 'filled';
|
|
225
|
+
out(`\n✅ Order filled!`);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
const orders = await client.getOpenOrders();
|
|
229
|
+
const ourOrder = orders.find(o => o.oid === currentOid);
|
|
230
|
+
if (!ourOrder) {
|
|
231
|
+
applyFills(currentOid);
|
|
232
|
+
if (remainingSize <= opts.size * 0.001) {
|
|
233
|
+
filled = true;
|
|
234
|
+
exitReason = 'filled';
|
|
235
|
+
out(`\n✅ Order filled!`);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
currentOid = null;
|
|
239
|
+
lastPrice = null;
|
|
240
|
+
}
|
|
152
241
|
}
|
|
153
242
|
}
|
|
243
|
+
await sleep(intervalMs);
|
|
154
244
|
}
|
|
155
|
-
await sleep(intervalMs);
|
|
156
245
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
246
|
+
finally {
|
|
247
|
+
// Always pull the working order before returning — even when the loop
|
|
248
|
+
// throws — so an error never strands a live resting quote at a stale price.
|
|
249
|
+
if (currentOid !== null && !filled) {
|
|
250
|
+
applyFills(currentOid);
|
|
251
|
+
out(`\nCancelling unfilled order...`);
|
|
252
|
+
try {
|
|
253
|
+
await client.cancel(opts.coin, currentOid);
|
|
254
|
+
out(`✅ Cancelled`);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
out(`⚠️ Could not cancel (may have filled)`);
|
|
258
|
+
}
|
|
259
|
+
applyFills(currentOid);
|
|
260
|
+
if (remainingSize <= opts.size * 0.001) {
|
|
261
|
+
filled = true;
|
|
262
|
+
exitReason = 'filled';
|
|
263
|
+
}
|
|
165
264
|
}
|
|
265
|
+
if (ownsFillWatcher)
|
|
266
|
+
await fillWatcher.stop();
|
|
166
267
|
}
|
|
167
268
|
const elapsed = (Date.now() - startTime) / 1000;
|
|
168
269
|
const endMid = parseFloat((await client.getAllMids())[opts.coin]);
|
|
@@ -172,6 +273,7 @@ export async function runChase(opts) {
|
|
|
172
273
|
out(`Iterations: ${iteration}`);
|
|
173
274
|
out(`Start Mid: ${formatUsd(startMid)}`);
|
|
174
275
|
out(`End Mid: ${formatUsd(endMid)} (${priceMove >= 0 ? '+' : ''}${priceMove.toFixed(1)} bps)`);
|
|
276
|
+
out(`Filled Size: ${(opts.size - remainingSize).toFixed(6)} / ${opts.size}`);
|
|
175
277
|
out(`Result: ${filled ? '✅ Filled' : '❌ Not filled'}`);
|
|
176
278
|
return { status: exitReason, iterations: iteration, durationSec: elapsed, startMid, endMid };
|
|
177
279
|
}
|
|
@@ -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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { WebSocketManager } from '../core/ws.js';
|
|
2
|
+
import { sleep } from '../core/utils.js';
|
|
3
|
+
export class UserFillWatcher {
|
|
4
|
+
client;
|
|
5
|
+
fills = new Map();
|
|
6
|
+
seenFills = new Set();
|
|
7
|
+
ws;
|
|
8
|
+
ownsWs;
|
|
9
|
+
started = false;
|
|
10
|
+
sinceMs;
|
|
11
|
+
user;
|
|
12
|
+
onFill = (fill) => {
|
|
13
|
+
this.recordFill({
|
|
14
|
+
oid: fill.oid,
|
|
15
|
+
coin: fill.coin,
|
|
16
|
+
px: fill.px,
|
|
17
|
+
sz: fill.sz,
|
|
18
|
+
time: fill.time,
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
constructor(client, options = {}) {
|
|
22
|
+
this.client = client;
|
|
23
|
+
this.ws = options.ws === undefined ? new WebSocketManager(client.verbose) : options.ws;
|
|
24
|
+
this.ownsWs = options.ws === undefined;
|
|
25
|
+
this.sinceMs = options.sinceMs ?? Date.now();
|
|
26
|
+
this.user = (options.user ?? client.address);
|
|
27
|
+
}
|
|
28
|
+
async start() {
|
|
29
|
+
if (this.started)
|
|
30
|
+
return;
|
|
31
|
+
this.started = true;
|
|
32
|
+
if (!this.ws)
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
await this.ws.connect();
|
|
36
|
+
this.ws.on('userFill', this.onFill);
|
|
37
|
+
await this.ws.subscribeUserFills(this.user);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
this.ws.off('userFill', this.onFill);
|
|
41
|
+
if (this.ownsWs)
|
|
42
|
+
await this.ws.close().catch(() => { });
|
|
43
|
+
this.ws = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async stop() {
|
|
47
|
+
if (!this.started)
|
|
48
|
+
return;
|
|
49
|
+
this.started = false;
|
|
50
|
+
if (!this.ws)
|
|
51
|
+
return;
|
|
52
|
+
this.ws.off('userFill', this.onFill);
|
|
53
|
+
if (this.ownsWs)
|
|
54
|
+
await this.ws.close().catch(() => { });
|
|
55
|
+
}
|
|
56
|
+
getFilled(oid) {
|
|
57
|
+
return this.fills.get(oid) ?? { size: 0, notional: 0 };
|
|
58
|
+
}
|
|
59
|
+
async waitForFill(oid, targetSize, timeoutMs, options = {}) {
|
|
60
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
61
|
+
const pollMs = Math.max(250, options.pollMs ?? 1000);
|
|
62
|
+
while (Date.now() <= deadline) {
|
|
63
|
+
await this.refreshRestFills(options.coin);
|
|
64
|
+
const filled = this.getFilled(oid);
|
|
65
|
+
if (filled.size >= targetSize * 0.999)
|
|
66
|
+
return filled;
|
|
67
|
+
await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
|
|
68
|
+
}
|
|
69
|
+
await this.refreshRestFills(options.coin);
|
|
70
|
+
return this.getFilled(oid);
|
|
71
|
+
}
|
|
72
|
+
async refreshRestFills(coin) {
|
|
73
|
+
try {
|
|
74
|
+
const fills = await this.client.getUserFills(this.user);
|
|
75
|
+
for (const fill of fills) {
|
|
76
|
+
this.recordFill(fill, coin);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// WebSocket is the primary path; REST polling is best-effort fallback.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
recordFill(fill, expectedCoin) {
|
|
84
|
+
if (expectedCoin && fill.coin !== expectedCoin)
|
|
85
|
+
return;
|
|
86
|
+
if (fill.time < this.sinceMs)
|
|
87
|
+
return;
|
|
88
|
+
const key = `${fill.oid}:${fill.time}:${fill.px}:${fill.sz}`;
|
|
89
|
+
if (this.seenFills.has(key))
|
|
90
|
+
return;
|
|
91
|
+
const size = parseFloat(fill.sz);
|
|
92
|
+
const price = parseFloat(fill.px);
|
|
93
|
+
if (!Number.isFinite(size) || size <= 0 || !Number.isFinite(price) || price <= 0)
|
|
94
|
+
return;
|
|
95
|
+
this.seenFills.add(key);
|
|
96
|
+
const prev = this.fills.get(fill.oid) ?? { size: 0, notional: 0 };
|
|
97
|
+
const next = {
|
|
98
|
+
size: prev.size + size,
|
|
99
|
+
notional: prev.notional + size * price,
|
|
100
|
+
};
|
|
101
|
+
this.fills.set(fill.oid, {
|
|
102
|
+
...next,
|
|
103
|
+
avgPrice: next.notional / next.size,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,3 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
|
-
|
|
2
|
+
import type { CancelResponse, OrderResponse } from '../core/types.js';
|
|
3
|
+
export interface OrderLevel {
|
|
4
|
+
level: number;
|
|
5
|
+
price: number;
|
|
6
|
+
size: number;
|
|
7
|
+
distanceFromMid: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ScaleClient {
|
|
10
|
+
verbose: boolean;
|
|
11
|
+
getAllMids(): Promise<Record<string, string>>;
|
|
12
|
+
bulkOrder(orders: Array<{
|
|
13
|
+
coin: string;
|
|
14
|
+
isBuy: boolean;
|
|
15
|
+
size: number;
|
|
16
|
+
price: number;
|
|
17
|
+
tif?: 'Gtc' | 'Alo';
|
|
18
|
+
reduceOnly?: boolean;
|
|
19
|
+
leverage?: number;
|
|
20
|
+
}>): Promise<OrderResponse>;
|
|
21
|
+
bulkCancel(cancels: Array<{
|
|
22
|
+
coin: string;
|
|
23
|
+
oid: number;
|
|
24
|
+
}>): Promise<CancelResponse>;
|
|
25
|
+
}
|
|
26
|
+
export interface ScaleOptions {
|
|
27
|
+
coin: string;
|
|
28
|
+
side: 'buy' | 'sell';
|
|
29
|
+
size: number;
|
|
30
|
+
levels: number;
|
|
31
|
+
rangePct: number;
|
|
32
|
+
distribution?: 'linear' | 'exponential' | 'flat';
|
|
33
|
+
leverage?: number;
|
|
34
|
+
reduceOnly?: boolean;
|
|
35
|
+
tif?: 'Gtc' | 'Alo';
|
|
36
|
+
dryRun?: boolean;
|
|
37
|
+
verbose?: boolean;
|
|
38
|
+
rollbackOnPartial?: boolean;
|
|
39
|
+
client?: ScaleClient;
|
|
40
|
+
output?: (line: string) => void;
|
|
41
|
+
}
|
|
42
|
+
export interface ScaleResult {
|
|
43
|
+
status: 'dry' | 'complete' | 'partial' | 'failed';
|
|
44
|
+
levels: OrderLevel[];
|
|
45
|
+
restingOids: number[];
|
|
46
|
+
filledOids: number[];
|
|
47
|
+
errors: string[];
|
|
48
|
+
rolledBack: boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare function calculateLevels(midPrice: number, isBuy: boolean, totalSize: number, numLevels: number, rangePct: number, distribution: 'linear' | 'exponential' | 'flat'): OrderLevel[];
|
|
51
|
+
export declare function runScale(opts: ScaleOptions): Promise<ScaleResult>;
|
|
3
52
|
//# sourceMappingURL=scale.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scale.d.ts","sourceRoot":"","sources":["../../scripts/operations/scale.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"scale.d.ts","sourceRoot":"","sources":["../../scripts/operations/scale.ts"],"names":[],"mappings":";AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAyCtE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,SAAS,CACP,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GACzI,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1B,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACpF;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,QAAQ,GAAG,aAAa,GAAG,MAAM,CAAC;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAClD,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,QAAQ,GAAG,aAAa,GAAG,MAAM,GAC9C,UAAU,EAAE,CA0Cd;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAsIvE"}
|