@tradejs/cli 2.0.13 → 2.0.14

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.
@@ -0,0 +1,476 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/scripts/hyperliquidWhaleBackfill.ts
31
+ var hyperliquidWhaleBackfill_exports = {};
32
+ __export(hyperliquidWhaleBackfill_exports, {
33
+ main: () => main
34
+ });
35
+ module.exports = __toCommonJS(hyperliquidWhaleBackfill_exports);
36
+ var import_args = __toESM(require("args"));
37
+
38
+ // src/lib/hyperliquidWhaleBackfill.ts
39
+ var import_timescale = require("@tradejs/infra/timescale");
40
+ var import_strategies = require("@tradejs/node/strategies");
41
+
42
+ // src/lib/hyperliquidWhaleData.ts
43
+ var HYPERLIQUID_WHALE_BUCKET_MS = 6e4;
44
+ var finiteNumber = (value) => {
45
+ const numeric = typeof value === "number" ? value : Number(value);
46
+ return Number.isFinite(numeric) ? numeric : null;
47
+ };
48
+ var POSITION_EPSILON = 1e-12;
49
+ var normalizePosition = (value) => Math.abs(value) <= POSITION_EPSILON ? 0 : value;
50
+ var classifyHyperliquidPositionChange = (params) => {
51
+ const startValue = finiteNumber(params.startPosition);
52
+ const size = finiteNumber(params.size);
53
+ const side = String(params.side ?? "").toUpperCase();
54
+ if (startValue == null || size == null || size <= 0) return null;
55
+ if (side !== "B" && side !== "A") return null;
56
+ const startPosition = normalizePosition(startValue);
57
+ const endPosition = normalizePosition(
58
+ startPosition + (side === "B" ? size : -size)
59
+ );
60
+ const startLong = Math.max(startPosition, 0);
61
+ const endLong = Math.max(endPosition, 0);
62
+ const startShort = Math.max(-startPosition, 0);
63
+ const endShort = Math.max(-endPosition, 0);
64
+ const longEntrySize = Math.max(0, endLong - startLong);
65
+ const shortEntrySize = Math.max(0, endShort - startShort);
66
+ const longExitSize = Math.max(0, startLong - endLong);
67
+ const shortExitSize = Math.max(0, startShort - endShort);
68
+ const flipped = startPosition * endPosition < 0;
69
+ const opened = startPosition === 0;
70
+ const closed = endPosition === 0;
71
+ const increased = Math.sign(startPosition) === Math.sign(endPosition) && Math.abs(endPosition) > Math.abs(startPosition);
72
+ return {
73
+ startPosition,
74
+ endPosition,
75
+ action: flipped ? "flip" : opened ? "open" : closed ? "close" : increased ? "increase" : "reduce",
76
+ longEntrySize,
77
+ shortEntrySize,
78
+ longExitSize,
79
+ shortExitSize
80
+ };
81
+ };
82
+ var normalizeAddress = (value) => typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value) ? value.toLowerCase() : null;
83
+ var normalizeTradeFields = (value) => {
84
+ const symbol = typeof value.coin === "string" ? value.coin.trim() : "";
85
+ const price = finiteNumber(value.px);
86
+ const size = finiteNumber(value.sz);
87
+ const timestamp = finiteNumber(value.time);
88
+ const tid = typeof value.tid === "string" || typeof value.tid === "number" ? String(value.tid) : "";
89
+ if (!symbol || price == null || size == null || timestamp == null || !tid || price <= 0 || size <= 0) {
90
+ return null;
91
+ }
92
+ return {
93
+ symbol,
94
+ price,
95
+ size,
96
+ timestamp,
97
+ tid,
98
+ notionalUsd: price * size
99
+ };
100
+ };
101
+ var normalizeHyperliquidUserFill = (params) => {
102
+ const normalized = normalizeTradeFields(params.fill);
103
+ if (!normalized || !params.trackedSymbols.has(normalized.symbol)) return null;
104
+ const address = normalizeAddress(params.address);
105
+ if (!address) return null;
106
+ const side = String(params.fill.side ?? "").toUpperCase();
107
+ if (side !== "B" && side !== "A") return null;
108
+ const positionChange = classifyHyperliquidPositionChange({
109
+ startPosition: params.fill.startPosition,
110
+ side,
111
+ size: normalized.size
112
+ });
113
+ const closedPnl = finiteNumber(params.fill.closedPnl);
114
+ const liquidation = params.fill.liquidation != null && typeof params.fill.liquidation === "object";
115
+ const positionFields = positionChange ? side === "B" ? {
116
+ buyerStartPosition: positionChange.startPosition,
117
+ buyerEndPosition: positionChange.endPosition,
118
+ buyerPositionAction: positionChange.action,
119
+ buyerClosedPnl: closedPnl,
120
+ buyerLiquidation: liquidation
121
+ } : {
122
+ sellerStartPosition: positionChange.startPosition,
123
+ sellerEndPosition: positionChange.endPosition,
124
+ sellerPositionAction: positionChange.action,
125
+ sellerClosedPnl: closedPnl,
126
+ sellerLiquidation: liquidation
127
+ } : {};
128
+ return {
129
+ symbol: normalized.symbol,
130
+ ts: new Date(normalized.timestamp),
131
+ tid: normalized.tid,
132
+ price: normalized.price,
133
+ size: normalized.size,
134
+ notionalUsd: normalized.notionalUsd,
135
+ buyerAddress: side === "B" ? address : null,
136
+ sellerAddress: side === "A" ? address : null,
137
+ buyerTracked: side === "B",
138
+ sellerTracked: side === "A",
139
+ ...positionFields,
140
+ ...params.identity,
141
+ source: "hyperliquid_user_fills"
142
+ };
143
+ };
144
+
145
+ // src/lib/hyperliquidRateLimiter.ts
146
+ var DEFAULT_WEIGHT_PER_MINUTE = 1e3;
147
+ var WINDOW_MS = 6e4;
148
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
149
+ var HyperliquidInfoRateLimiter = class {
150
+ constructor(capacity = DEFAULT_WEIGHT_PER_MINUTE, now = Date.now, wait = sleep) {
151
+ this.capacity = capacity;
152
+ this.now = now;
153
+ this.wait = wait;
154
+ this.reservations = [];
155
+ this.queue = Promise.resolve();
156
+ }
157
+ reserve(weight) {
158
+ const normalizedWeight = Math.max(1, Math.ceil(weight));
159
+ const pending = this.queue.then(
160
+ () => this.reserveInternal(normalizedWeight)
161
+ );
162
+ this.queue = pending.catch(() => void 0);
163
+ return pending;
164
+ }
165
+ async reserveInternal(weight) {
166
+ if (weight > this.capacity) {
167
+ throw new Error(
168
+ `Hyperliquid request weight ${weight} exceeds limiter capacity ${this.capacity}`
169
+ );
170
+ }
171
+ for (; ; ) {
172
+ const nowMs = this.now();
173
+ this.reservations = this.reservations.filter(
174
+ (reservation) => reservation.atMs > nowMs - WINDOW_MS
175
+ );
176
+ const used = this.reservations.reduce(
177
+ (sum, reservation) => sum + reservation.weight,
178
+ 0
179
+ );
180
+ if (used + weight <= this.capacity) {
181
+ this.reservations.push({ atMs: nowMs, weight });
182
+ return;
183
+ }
184
+ const oldest = this.reservations[0];
185
+ await this.wait(Math.max(1, oldest.atMs + WINDOW_MS - nowMs));
186
+ }
187
+ }
188
+ };
189
+ var hyperliquidInfoResponseWeight = (rows) => Math.max(0, Math.ceil(Math.max(0, rows) / 20));
190
+
191
+ // src/lib/hyperliquidWhaleBackfill.ts
192
+ var DEFAULT_BASE_URL = "https://api.hyperliquid.xyz";
193
+ var DEFAULT_CONCURRENCY = 2;
194
+ var DEFAULT_RATE_LIMIT_WEIGHT = 1e3;
195
+ var MAX_PAGE_SIZE = 2e3;
196
+ var MAX_PAGES_PER_WHALE = 5;
197
+ var REQUEST_WEIGHT = 20;
198
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
199
+ var positiveInt = (value, fallback) => {
200
+ const parsed = Number.parseInt(String(value ?? ""), 10);
201
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
202
+ };
203
+ var minuteCeil = (value) => Math.ceil(value / HYPERLIQUID_WHALE_BUCKET_MS) * HYPERLIQUID_WHALE_BUCKET_MS;
204
+ var nextMinute = (value) => (Math.floor(value / HYPERLIQUID_WHALE_BUCKET_MS) + 1) * HYPERLIQUID_WHALE_BUCKET_MS;
205
+ var fetchHyperliquidUserFillsByTime = async (params) => {
206
+ const fetchImpl = params.fetchImpl ?? fetch;
207
+ const limiter = params.rateLimiter ?? new HyperliquidInfoRateLimiter(
208
+ positiveInt(
209
+ process.env.HYPERLIQUID_WHALE_RATE_LIMIT_WEIGHT,
210
+ DEFAULT_RATE_LIMIT_WEIGHT
211
+ ),
212
+ Date.now,
213
+ params.wait ?? sleep2
214
+ );
215
+ const fills = [];
216
+ let cursor = params.startTime;
217
+ for (let page = 0; page < (params.maxPages ?? MAX_PAGES_PER_WHALE) && cursor <= params.endTime; page += 1) {
218
+ await limiter.reserve(REQUEST_WEIGHT);
219
+ const response = await fetchImpl(
220
+ `${process.env.HYPERLIQUID_API_BASE_URL?.trim() || DEFAULT_BASE_URL}/info`,
221
+ {
222
+ method: "POST",
223
+ headers: { "content-type": "application/json" },
224
+ body: JSON.stringify({
225
+ type: "userFillsByTime",
226
+ user: params.address,
227
+ startTime: cursor,
228
+ endTime: params.endTime,
229
+ aggregateByTime: true
230
+ })
231
+ }
232
+ );
233
+ if (!response.ok) {
234
+ throw new Error(
235
+ `Hyperliquid userFillsByTime ${response.status}: ${await response.text()}`
236
+ );
237
+ }
238
+ const raw = await response.json();
239
+ const pageRows = Array.isArray(raw) ? raw : [];
240
+ const responseWeight = hyperliquidInfoResponseWeight(pageRows.length);
241
+ if (responseWeight > 0) await limiter.reserve(responseWeight);
242
+ fills.push(...pageRows);
243
+ if (pageRows.length < MAX_PAGE_SIZE) {
244
+ return { fills, truncated: false, coveredFromMs: params.startTime };
245
+ }
246
+ const lastTime = Math.max(
247
+ ...pageRows.map((row) => Number(row.time)).filter(Number.isFinite)
248
+ );
249
+ if (!Number.isFinite(lastTime) || lastTime < cursor) {
250
+ throw new Error(
251
+ `Hyperliquid fills pagination stalled for ${params.address}`
252
+ );
253
+ }
254
+ cursor = lastTime + 1;
255
+ }
256
+ if (cursor > params.endTime) {
257
+ return { fills, truncated: false, coveredFromMs: params.startTime };
258
+ }
259
+ const firstAvailableTime = Math.min(
260
+ ...fills.map((fill) => Number(fill.time)).filter(Number.isFinite)
261
+ );
262
+ return {
263
+ fills,
264
+ truncated: true,
265
+ coveredFromMs: Math.min(params.endTime + 1, nextMinute(firstAvailableTime))
266
+ };
267
+ };
268
+ var runConcurrent = async (params) => {
269
+ let nextIndex = 0;
270
+ await Promise.all(
271
+ Array.from(
272
+ { length: Math.min(params.concurrency, params.items.length) },
273
+ async () => {
274
+ for (; ; ) {
275
+ const index = nextIndex;
276
+ nextIndex += 1;
277
+ if (index >= params.items.length) return;
278
+ await params.worker(params.items[index], index);
279
+ }
280
+ }
281
+ )
282
+ );
283
+ };
284
+ var backfillHyperliquidWhaleContext = async (params) => {
285
+ const universe = (0, import_strategies.getHyperliquidPerpUniverseSnapshot)();
286
+ const whales = (0, import_strategies.getHyperliquidWhaleRegistrySnapshot)();
287
+ const trackedSymbols = new Set(universe.symbols);
288
+ const identity = {
289
+ universeFingerprint: universe.fingerprint,
290
+ whaleRegistryFingerprint: whales.fingerprint
291
+ };
292
+ const fromMs = Math.floor(params.startMs / HYPERLIQUID_WHALE_BUCKET_MS) * HYPERLIQUID_WHALE_BUCKET_MS;
293
+ const toMs = minuteCeil(params.endMs);
294
+ const cached = await (0, import_timescale.hasHyperliquidWhaleBackfillCoverage)({
295
+ fromMs,
296
+ toMs,
297
+ ...identity
298
+ });
299
+ const emptyResult = () => ({
300
+ cached,
301
+ cacheOnly: params.cacheOnly,
302
+ fills: 0,
303
+ events: 0,
304
+ buckets: 0,
305
+ coverageBuckets: 0,
306
+ complete: cached,
307
+ walletsProcessed: 0,
308
+ walletsCached: 0,
309
+ completeWallets: 0,
310
+ truncatedWallets: 0,
311
+ failedWallets: 0,
312
+ fromMs,
313
+ toMs
314
+ });
315
+ if (cached || params.cacheOnly) return emptyResult();
316
+ const limiter = new HyperliquidInfoRateLimiter(
317
+ positiveInt(
318
+ process.env.HYPERLIQUID_WHALE_RATE_LIMIT_WEIGHT,
319
+ DEFAULT_RATE_LIMIT_WEIGHT
320
+ ),
321
+ Date.now,
322
+ params.wait ?? sleep2
323
+ );
324
+ const concurrency = Math.min(
325
+ 4,
326
+ positiveInt(
327
+ params.concurrency ?? process.env.HYPERLIQUID_WHALE_CONCURRENCY,
328
+ DEFAULT_CONCURRENCY
329
+ )
330
+ );
331
+ let fills = 0;
332
+ let events = 0;
333
+ let walletsProcessed = 0;
334
+ let walletsCached = 0;
335
+ let completeWallets = 0;
336
+ let truncatedWallets = 0;
337
+ let failedWallets = 0;
338
+ const failures = [];
339
+ await runConcurrent({
340
+ items: whales.addresses,
341
+ concurrency,
342
+ worker: async (address) => {
343
+ const previous = await (0, import_timescale.getHyperliquidWhaleWalletCoverage)({
344
+ address,
345
+ fromMs,
346
+ toMs,
347
+ ...identity
348
+ });
349
+ if (previous && previous.status !== "failed") {
350
+ walletsCached += 1;
351
+ if (previous.status === "complete") completeWallets += 1;
352
+ else truncatedWallets += 1;
353
+ } else {
354
+ try {
355
+ const result2 = await fetchHyperliquidUserFillsByTime({
356
+ address,
357
+ startTime: fromMs,
358
+ endTime: toMs - 1,
359
+ fetchImpl: params.fetchImpl,
360
+ rateLimiter: limiter
361
+ });
362
+ const addressEvents = result2.fills.map(
363
+ (fill) => normalizeHyperliquidUserFill({
364
+ fill,
365
+ address,
366
+ trackedSymbols,
367
+ identity
368
+ })
369
+ ).filter((row) => row != null);
370
+ await (0, import_timescale.upsertHyperliquidWhaleTradeEvents)(addressEvents);
371
+ await (0, import_timescale.upsertHyperliquidWhaleWalletCoverage)({
372
+ address,
373
+ fromMs,
374
+ toMs,
375
+ coveredFromMs: result2.coveredFromMs,
376
+ coveredToMs: toMs,
377
+ status: result2.truncated ? "truncated" : "complete",
378
+ fillsCount: result2.fills.length,
379
+ ...identity
380
+ });
381
+ fills += result2.fills.length;
382
+ events += addressEvents.length;
383
+ if (result2.truncated) truncatedWallets += 1;
384
+ else completeWallets += 1;
385
+ } catch (error) {
386
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
387
+ failures.push(normalizedError);
388
+ failedWallets += 1;
389
+ await (0, import_timescale.upsertHyperliquidWhaleWalletCoverage)({
390
+ address,
391
+ fromMs,
392
+ toMs,
393
+ coveredFromMs: null,
394
+ coveredToMs: null,
395
+ status: "failed",
396
+ fillsCount: 0,
397
+ error: normalizedError.message,
398
+ ...identity
399
+ });
400
+ }
401
+ }
402
+ walletsProcessed += 1;
403
+ params.log?.(
404
+ `Hyperliquid whales ${walletsProcessed}/${whales.addresses.length}: complete=${completeWallets}, truncated=${truncatedWallets}, failed=${failedWallets}, fills=${fills}, events=${events}`
405
+ );
406
+ }
407
+ });
408
+ const buckets = await (0, import_timescale.rebuildHyperliquidWhaleFlowRows)({
409
+ fromMs,
410
+ toMs,
411
+ ...identity
412
+ });
413
+ const coverageBuckets = await (0, import_timescale.rebuildHyperliquidWhaleCoverageRows)({
414
+ fromMs,
415
+ toMs,
416
+ expectedWhales: whales.addresses.length,
417
+ ...identity
418
+ });
419
+ const result = {
420
+ cached: false,
421
+ cacheOnly: false,
422
+ fills,
423
+ events,
424
+ buckets,
425
+ coverageBuckets,
426
+ complete: truncatedWallets === 0 && failedWallets === 0,
427
+ walletsProcessed,
428
+ walletsCached,
429
+ completeWallets,
430
+ truncatedWallets,
431
+ failedWallets,
432
+ fromMs,
433
+ toMs
434
+ };
435
+ if (params.strict !== false && failures.length) {
436
+ throw new AggregateError(
437
+ failures,
438
+ `Hyperliquid whale backfill failed for ${failures.length}/${whales.addresses.length} wallets`
439
+ );
440
+ }
441
+ return result;
442
+ };
443
+
444
+ // src/scripts/hyperliquidWhaleBackfill.ts
445
+ import_args.default.option(["d", "days"], "Historical lookback in days", 1);
446
+ import_args.default.option(["f", "from"], "Inclusive ISO timestamp");
447
+ import_args.default.option(["t", "to"], "Exclusive ISO timestamp");
448
+ import_args.default.option(
449
+ ["c", "cacheOnly"],
450
+ "Do not call Hyperliquid when coverage is missing"
451
+ );
452
+ var flags = import_args.default.parse(process.argv);
453
+ var parseTime = (value, fallback) => {
454
+ if (value == null || value === "") return fallback;
455
+ const parsed = Date.parse(String(value));
456
+ if (!Number.isFinite(parsed)) throw new Error(`Invalid timestamp: ${value}`);
457
+ return parsed;
458
+ };
459
+ var main = async () => {
460
+ const toMs = parseTime(flags.to, Date.now());
461
+ const days = Number(flags.days);
462
+ const lookbackMs = (Number.isFinite(days) && days > 0 ? days : 1) * 864e5;
463
+ const startMs = parseTime(flags.from, toMs - lookbackMs);
464
+ if (startMs >= toMs) throw new Error("--from must be before --to");
465
+ const result = await backfillHyperliquidWhaleContext({
466
+ startMs,
467
+ endMs: toMs,
468
+ cacheOnly: Boolean(flags.cacheOnly),
469
+ log: console.log
470
+ });
471
+ console.log(JSON.stringify(result, null, 2));
472
+ };
473
+ // Annotate the CommonJS export names for ESM import in node:
474
+ 0 && (module.exports = {
475
+ main
476
+ });