@tradejs/cli 2.0.12 → 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,333 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/scripts/hyperliquidWhaleIngest.ts
21
+ var hyperliquidWhaleIngest_exports = {};
22
+ __export(hyperliquidWhaleIngest_exports, {
23
+ main: () => main
24
+ });
25
+ module.exports = __toCommonJS(hyperliquidWhaleIngest_exports);
26
+
27
+ // src/lib/hyperliquidWhaleStream.ts
28
+ var import_ws = require("ws");
29
+ var import_timescale = require("@tradejs/infra/timescale");
30
+ var import_strategies = require("@tradejs/node/strategies");
31
+
32
+ // src/lib/hyperliquidWhaleData.ts
33
+ var HYPERLIQUID_WHALE_BUCKET_MS = 6e4;
34
+ var finiteNumber = (value) => {
35
+ const numeric = typeof value === "number" ? value : Number(value);
36
+ return Number.isFinite(numeric) ? numeric : null;
37
+ };
38
+ var POSITION_EPSILON = 1e-12;
39
+ var normalizePosition = (value) => Math.abs(value) <= POSITION_EPSILON ? 0 : value;
40
+ var classifyHyperliquidPositionChange = (params) => {
41
+ const startValue = finiteNumber(params.startPosition);
42
+ const size = finiteNumber(params.size);
43
+ const side = String(params.side ?? "").toUpperCase();
44
+ if (startValue == null || size == null || size <= 0) return null;
45
+ if (side !== "B" && side !== "A") return null;
46
+ const startPosition = normalizePosition(startValue);
47
+ const endPosition = normalizePosition(
48
+ startPosition + (side === "B" ? size : -size)
49
+ );
50
+ const startLong = Math.max(startPosition, 0);
51
+ const endLong = Math.max(endPosition, 0);
52
+ const startShort = Math.max(-startPosition, 0);
53
+ const endShort = Math.max(-endPosition, 0);
54
+ const longEntrySize = Math.max(0, endLong - startLong);
55
+ const shortEntrySize = Math.max(0, endShort - startShort);
56
+ const longExitSize = Math.max(0, startLong - endLong);
57
+ const shortExitSize = Math.max(0, startShort - endShort);
58
+ const flipped = startPosition * endPosition < 0;
59
+ const opened = startPosition === 0;
60
+ const closed = endPosition === 0;
61
+ const increased = Math.sign(startPosition) === Math.sign(endPosition) && Math.abs(endPosition) > Math.abs(startPosition);
62
+ return {
63
+ startPosition,
64
+ endPosition,
65
+ action: flipped ? "flip" : opened ? "open" : closed ? "close" : increased ? "increase" : "reduce",
66
+ longEntrySize,
67
+ shortEntrySize,
68
+ longExitSize,
69
+ shortExitSize
70
+ };
71
+ };
72
+ var normalizeAddress = (value) => typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value) ? value.toLowerCase() : null;
73
+ var normalizeTradeFields = (value) => {
74
+ const symbol = typeof value.coin === "string" ? value.coin.trim() : "";
75
+ const price = finiteNumber(value.px);
76
+ const size = finiteNumber(value.sz);
77
+ const timestamp = finiteNumber(value.time);
78
+ const tid = typeof value.tid === "string" || typeof value.tid === "number" ? String(value.tid) : "";
79
+ if (!symbol || price == null || size == null || timestamp == null || !tid || price <= 0 || size <= 0) {
80
+ return null;
81
+ }
82
+ return {
83
+ symbol,
84
+ price,
85
+ size,
86
+ timestamp,
87
+ tid,
88
+ notionalUsd: price * size
89
+ };
90
+ };
91
+ var normalizeHyperliquidUserFill = (params) => {
92
+ const normalized = normalizeTradeFields(params.fill);
93
+ if (!normalized || !params.trackedSymbols.has(normalized.symbol)) return null;
94
+ const address = normalizeAddress(params.address);
95
+ if (!address) return null;
96
+ const side = String(params.fill.side ?? "").toUpperCase();
97
+ if (side !== "B" && side !== "A") return null;
98
+ const positionChange = classifyHyperliquidPositionChange({
99
+ startPosition: params.fill.startPosition,
100
+ side,
101
+ size: normalized.size
102
+ });
103
+ const closedPnl = finiteNumber(params.fill.closedPnl);
104
+ const liquidation = params.fill.liquidation != null && typeof params.fill.liquidation === "object";
105
+ const positionFields = positionChange ? side === "B" ? {
106
+ buyerStartPosition: positionChange.startPosition,
107
+ buyerEndPosition: positionChange.endPosition,
108
+ buyerPositionAction: positionChange.action,
109
+ buyerClosedPnl: closedPnl,
110
+ buyerLiquidation: liquidation
111
+ } : {
112
+ sellerStartPosition: positionChange.startPosition,
113
+ sellerEndPosition: positionChange.endPosition,
114
+ sellerPositionAction: positionChange.action,
115
+ sellerClosedPnl: closedPnl,
116
+ sellerLiquidation: liquidation
117
+ } : {};
118
+ return {
119
+ symbol: normalized.symbol,
120
+ ts: new Date(normalized.timestamp),
121
+ tid: normalized.tid,
122
+ price: normalized.price,
123
+ size: normalized.size,
124
+ notionalUsd: normalized.notionalUsd,
125
+ buyerAddress: side === "B" ? address : null,
126
+ sellerAddress: side === "A" ? address : null,
127
+ buyerTracked: side === "B",
128
+ sellerTracked: side === "A",
129
+ ...positionFields,
130
+ ...params.identity,
131
+ source: "hyperliquid_user_fills"
132
+ };
133
+ };
134
+
135
+ // src/lib/hyperliquidWhaleStream.ts
136
+ var DEFAULT_WS_URL = "wss://api.hyperliquid.xyz/ws";
137
+ var EVENT_BATCH_SIZE = 500;
138
+ var FLUSH_INTERVAL_MS = 2e3;
139
+ var FINALIZE_INTERVAL_MS = 15e3;
140
+ var FINALIZE_LAG_MS = 5e3;
141
+ var REBUILD_LOOKBACK_MS = 10 * 6e4;
142
+ var RAW_EVENT_RETENTION_MS = 48 * 60 * 6e4;
143
+ var sleep = (ms, signal) => new Promise((resolve) => {
144
+ if (signal.aborted) return resolve();
145
+ const timer = setTimeout(resolve, ms);
146
+ signal.addEventListener(
147
+ "abort",
148
+ () => {
149
+ clearTimeout(timer);
150
+ resolve();
151
+ },
152
+ { once: true }
153
+ );
154
+ });
155
+ var parseHyperliquidUserFillsMessage = (raw) => {
156
+ try {
157
+ const message = JSON.parse(raw);
158
+ if (message.channel !== "userFills" || !message.data || typeof message.data !== "object" || Array.isArray(message.data)) {
159
+ return null;
160
+ }
161
+ const data = message.data;
162
+ if (typeof data.user !== "string" || !Array.isArray(data.fills)) {
163
+ return null;
164
+ }
165
+ return {
166
+ address: data.user,
167
+ isSnapshot: data.isSnapshot === true,
168
+ fills: data.fills
169
+ };
170
+ } catch {
171
+ return null;
172
+ }
173
+ };
174
+ var runHyperliquidWhaleStream = async (params) => {
175
+ const universe = (0, import_strategies.getHyperliquidPerpUniverseSnapshot)();
176
+ const whales = (0, import_strategies.getHyperliquidWhaleRegistrySnapshot)();
177
+ const trackedSymbols = new Set(universe.symbols);
178
+ const identity = {
179
+ universeFingerprint: universe.fingerprint,
180
+ whaleRegistryFingerprint: whales.fingerprint
181
+ };
182
+ const buffer = [];
183
+ let writes = Promise.resolve();
184
+ let streamCoverageFromMs = null;
185
+ let streamCoverageToMs = 0;
186
+ const flush = () => {
187
+ if (!buffer.length) return writes;
188
+ const batch = buffer.splice(0, buffer.length);
189
+ writes = writes.then(() => (0, import_timescale.upsertHyperliquidWhaleTradeEvents)(batch));
190
+ return writes;
191
+ };
192
+ const recordStreamCoverage = (toMs) => {
193
+ if (streamCoverageFromMs == null) return writes;
194
+ const fromMs = Math.max(streamCoverageFromMs, streamCoverageToMs);
195
+ if (toMs <= fromMs) return writes;
196
+ const rows = Array.from(
197
+ { length: Math.floor((toMs - fromMs) / HYPERLIQUID_WHALE_BUCKET_MS) },
198
+ (_, index) => ({
199
+ ts: new Date(fromMs + index * HYPERLIQUID_WHALE_BUCKET_MS),
200
+ coveredWhales: whales.addresses.length,
201
+ expectedWhales: whales.addresses.length,
202
+ coveragePct: 1,
203
+ source: "hyperliquid_user_fills_ws",
204
+ ...identity
205
+ })
206
+ );
207
+ streamCoverageToMs = toMs;
208
+ writes = writes.then(() => (0, import_timescale.upsertHyperliquidWhaleCoverageRows)(rows));
209
+ return writes;
210
+ };
211
+ const finalize = () => {
212
+ const toMs = Math.floor((Date.now() - FINALIZE_LAG_MS) / HYPERLIQUID_WHALE_BUCKET_MS) * HYPERLIQUID_WHALE_BUCKET_MS;
213
+ const fromMs = Math.max(0, toMs - REBUILD_LOOKBACK_MS);
214
+ recordStreamCoverage(toMs);
215
+ writes = writes.then(
216
+ () => (0, import_timescale.rebuildHyperliquidWhaleFlowRows)({
217
+ fromMs,
218
+ toMs,
219
+ deleteEventsBeforeMs: toMs - RAW_EVENT_RETENTION_MS,
220
+ ...identity
221
+ }).then((buckets) => {
222
+ params.log?.(`Hyperliquid whale-flow finalized buckets=${buckets}`);
223
+ })
224
+ );
225
+ return writes;
226
+ };
227
+ const flushTimer = setInterval(() => void flush(), FLUSH_INTERVAL_MS);
228
+ const finalizeTimer = setInterval(
229
+ () => void flush().then(finalize),
230
+ FINALIZE_INTERVAL_MS
231
+ );
232
+ let reconnectDelayMs = 1e3;
233
+ try {
234
+ while (!params.signal.aborted) {
235
+ await new Promise((resolve) => {
236
+ const ws = new import_ws.WebSocket(
237
+ process.env.HYPERLIQUID_WS_URL?.trim() || DEFAULT_WS_URL
238
+ );
239
+ let settled = false;
240
+ const finish = () => {
241
+ if (settled) return;
242
+ settled = true;
243
+ resolve();
244
+ };
245
+ const pingTimer = setInterval(() => {
246
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
247
+ ws.send(JSON.stringify({ method: "ping" }));
248
+ }
249
+ }, 3e4);
250
+ const close = () => {
251
+ clearInterval(pingTimer);
252
+ if (ws.readyState === import_ws.WebSocket.OPEN) ws.close();
253
+ else if (ws.readyState === import_ws.WebSocket.CONNECTING) ws.terminate();
254
+ finish();
255
+ };
256
+ params.signal.addEventListener("abort", close, { once: true });
257
+ ws.on("open", () => {
258
+ reconnectDelayMs = 1e3;
259
+ streamCoverageFromMs = Math.ceil(Date.now() / HYPERLIQUID_WHALE_BUCKET_MS) * HYPERLIQUID_WHALE_BUCKET_MS;
260
+ streamCoverageToMs = streamCoverageFromMs;
261
+ for (const address of whales.addresses) {
262
+ ws.send(
263
+ JSON.stringify({
264
+ method: "subscribe",
265
+ subscription: { type: "userFills", user: address }
266
+ })
267
+ );
268
+ }
269
+ params.log?.(
270
+ `Hyperliquid whale stream connected userFills subscriptions=${whales.addresses.length}`
271
+ );
272
+ });
273
+ ws.on("message", (data) => {
274
+ const message = parseHyperliquidUserFillsMessage(data.toString());
275
+ if (!message) return;
276
+ for (const fill of message.fills) {
277
+ const event = normalizeHyperliquidUserFill({
278
+ fill,
279
+ address: message.address,
280
+ trackedSymbols,
281
+ identity
282
+ });
283
+ if (event) buffer.push(event);
284
+ }
285
+ if (buffer.length >= EVENT_BATCH_SIZE) void flush();
286
+ });
287
+ ws.on("error", (error) => {
288
+ params.log?.(`Hyperliquid whale stream error: ${String(error)}`);
289
+ close();
290
+ });
291
+ ws.on("close", () => {
292
+ recordStreamCoverage(
293
+ Math.floor(Date.now() / HYPERLIQUID_WHALE_BUCKET_MS) * HYPERLIQUID_WHALE_BUCKET_MS
294
+ );
295
+ streamCoverageFromMs = null;
296
+ clearInterval(pingTimer);
297
+ params.signal.removeEventListener("abort", close);
298
+ finish();
299
+ });
300
+ });
301
+ if (!params.signal.aborted) {
302
+ await sleep(reconnectDelayMs, params.signal);
303
+ reconnectDelayMs = Math.min(3e4, reconnectDelayMs * 2);
304
+ }
305
+ }
306
+ } finally {
307
+ clearInterval(flushTimer);
308
+ clearInterval(finalizeTimer);
309
+ await flush();
310
+ await finalize();
311
+ }
312
+ };
313
+
314
+ // src/scripts/hyperliquidWhaleIngest.ts
315
+ var main = async () => {
316
+ const controller = new AbortController();
317
+ const stop = () => controller.abort();
318
+ process.once("SIGINT", stop);
319
+ process.once("SIGTERM", stop);
320
+ try {
321
+ await runHyperliquidWhaleStream({
322
+ signal: controller.signal,
323
+ log: console.log
324
+ });
325
+ } finally {
326
+ process.removeListener("SIGINT", stop);
327
+ process.removeListener("SIGTERM", stop);
328
+ }
329
+ };
330
+ // Annotate the CommonJS export names for ESM import in node:
331
+ 0 && (module.exports = {
332
+ main
333
+ });
@@ -0,0 +1,369 @@
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/hyperliquidWhalesUpdate.ts
31
+ var hyperliquidWhalesUpdate_exports = {};
32
+ __export(hyperliquidWhalesUpdate_exports, {
33
+ main: () => main
34
+ });
35
+ module.exports = __toCommonJS(hyperliquidWhalesUpdate_exports);
36
+ var import_config = require("dotenv/config");
37
+ var import_args = __toESM(require("args"));
38
+ var import_node_crypto = require("crypto");
39
+ var import_strategies2 = require("@tradejs/node/strategies");
40
+
41
+ // src/lib/hyperliquidWhaleBackfill.ts
42
+ var import_timescale = require("@tradejs/infra/timescale");
43
+ var import_strategies = require("@tradejs/node/strategies");
44
+
45
+ // src/lib/hyperliquidWhaleData.ts
46
+ var HYPERLIQUID_WHALE_BUCKET_MS = 6e4;
47
+
48
+ // src/lib/hyperliquidRateLimiter.ts
49
+ var DEFAULT_WEIGHT_PER_MINUTE = 1e3;
50
+ var WINDOW_MS = 6e4;
51
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
52
+ var HyperliquidInfoRateLimiter = class {
53
+ constructor(capacity = DEFAULT_WEIGHT_PER_MINUTE, now = Date.now, wait = sleep) {
54
+ this.capacity = capacity;
55
+ this.now = now;
56
+ this.wait = wait;
57
+ this.reservations = [];
58
+ this.queue = Promise.resolve();
59
+ }
60
+ reserve(weight) {
61
+ const normalizedWeight = Math.max(1, Math.ceil(weight));
62
+ const pending = this.queue.then(
63
+ () => this.reserveInternal(normalizedWeight)
64
+ );
65
+ this.queue = pending.catch(() => void 0);
66
+ return pending;
67
+ }
68
+ async reserveInternal(weight) {
69
+ if (weight > this.capacity) {
70
+ throw new Error(
71
+ `Hyperliquid request weight ${weight} exceeds limiter capacity ${this.capacity}`
72
+ );
73
+ }
74
+ for (; ; ) {
75
+ const nowMs = this.now();
76
+ this.reservations = this.reservations.filter(
77
+ (reservation) => reservation.atMs > nowMs - WINDOW_MS
78
+ );
79
+ const used = this.reservations.reduce(
80
+ (sum, reservation) => sum + reservation.weight,
81
+ 0
82
+ );
83
+ if (used + weight <= this.capacity) {
84
+ this.reservations.push({ atMs: nowMs, weight });
85
+ return;
86
+ }
87
+ const oldest = this.reservations[0];
88
+ await this.wait(Math.max(1, oldest.atMs + WINDOW_MS - nowMs));
89
+ }
90
+ }
91
+ };
92
+ var hyperliquidInfoResponseWeight = (rows) => Math.max(0, Math.ceil(Math.max(0, rows) / 20));
93
+
94
+ // src/lib/hyperliquidWhaleBackfill.ts
95
+ var DEFAULT_BASE_URL = "https://api.hyperliquid.xyz";
96
+ var DEFAULT_RATE_LIMIT_WEIGHT = 1e3;
97
+ var MAX_PAGE_SIZE = 2e3;
98
+ var MAX_PAGES_PER_WHALE = 5;
99
+ var REQUEST_WEIGHT = 20;
100
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
101
+ var positiveInt = (value, fallback) => {
102
+ const parsed = Number.parseInt(String(value ?? ""), 10);
103
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
104
+ };
105
+ var nextMinute = (value) => (Math.floor(value / HYPERLIQUID_WHALE_BUCKET_MS) + 1) * HYPERLIQUID_WHALE_BUCKET_MS;
106
+ var fetchHyperliquidUserFillsByTime = async (params) => {
107
+ const fetchImpl = params.fetchImpl ?? fetch;
108
+ const limiter = params.rateLimiter ?? new HyperliquidInfoRateLimiter(
109
+ positiveInt(
110
+ process.env.HYPERLIQUID_WHALE_RATE_LIMIT_WEIGHT,
111
+ DEFAULT_RATE_LIMIT_WEIGHT
112
+ ),
113
+ Date.now,
114
+ params.wait ?? sleep2
115
+ );
116
+ const fills = [];
117
+ let cursor = params.startTime;
118
+ for (let page = 0; page < (params.maxPages ?? MAX_PAGES_PER_WHALE) && cursor <= params.endTime; page += 1) {
119
+ await limiter.reserve(REQUEST_WEIGHT);
120
+ const response = await fetchImpl(
121
+ `${process.env.HYPERLIQUID_API_BASE_URL?.trim() || DEFAULT_BASE_URL}/info`,
122
+ {
123
+ method: "POST",
124
+ headers: { "content-type": "application/json" },
125
+ body: JSON.stringify({
126
+ type: "userFillsByTime",
127
+ user: params.address,
128
+ startTime: cursor,
129
+ endTime: params.endTime,
130
+ aggregateByTime: true
131
+ })
132
+ }
133
+ );
134
+ if (!response.ok) {
135
+ throw new Error(
136
+ `Hyperliquid userFillsByTime ${response.status}: ${await response.text()}`
137
+ );
138
+ }
139
+ const raw = await response.json();
140
+ const pageRows = Array.isArray(raw) ? raw : [];
141
+ const responseWeight = hyperliquidInfoResponseWeight(pageRows.length);
142
+ if (responseWeight > 0) await limiter.reserve(responseWeight);
143
+ fills.push(...pageRows);
144
+ if (pageRows.length < MAX_PAGE_SIZE) {
145
+ return { fills, truncated: false, coveredFromMs: params.startTime };
146
+ }
147
+ const lastTime = Math.max(
148
+ ...pageRows.map((row) => Number(row.time)).filter(Number.isFinite)
149
+ );
150
+ if (!Number.isFinite(lastTime) || lastTime < cursor) {
151
+ throw new Error(
152
+ `Hyperliquid fills pagination stalled for ${params.address}`
153
+ );
154
+ }
155
+ cursor = lastTime + 1;
156
+ }
157
+ if (cursor > params.endTime) {
158
+ return { fills, truncated: false, coveredFromMs: params.startTime };
159
+ }
160
+ const firstAvailableTime = Math.min(
161
+ ...fills.map((fill) => Number(fill.time)).filter(Number.isFinite)
162
+ );
163
+ return {
164
+ fills,
165
+ truncated: true,
166
+ coveredFromMs: Math.min(params.endTime + 1, nextMinute(firstAvailableTime))
167
+ };
168
+ };
169
+
170
+ // src/lib/hyperliquidWhaleSelection.ts
171
+ var HYPERLIQUID_WHALE_SELECTION = {
172
+ minimumAccountValueUsd: 5e5,
173
+ minimumActiveDays: 1,
174
+ maximumRawFillsPerDay: 20,
175
+ maximumDirectionalExecutionsPerDay: 10,
176
+ minimumMedianNotionalUsd: 0,
177
+ minimumMedianInterExecutionMinutes: 0,
178
+ minimumTop30NotionalShare: 1e-3,
179
+ minimumTurnoverToEquity: 0.05,
180
+ maximumTurnoverToEquity: 5
181
+ };
182
+ var finiteNumber = (value) => {
183
+ const parsed = Number(value);
184
+ return Number.isFinite(parsed) ? parsed : 0;
185
+ };
186
+ var median = (values) => {
187
+ if (!values.length) return 0;
188
+ const sorted = [...values].sort((left, right) => left - right);
189
+ const middle = Math.floor(sorted.length / 2);
190
+ return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
191
+ };
192
+ var evaluateHyperliquidWhaleStructure = (params) => {
193
+ const fills = params.fills.filter((fill) => {
194
+ const time = finiteNumber(fill.time);
195
+ return time >= params.calibrationFromMs && time < params.calibrationToMs;
196
+ }).sort((left, right) => finiteNumber(left.time) - finiteNumber(right.time));
197
+ const days = Math.max(
198
+ 1,
199
+ (params.calibrationToMs - params.calibrationFromMs) / 864e5
200
+ );
201
+ const executions = fills.reduce((clusters, fill) => {
202
+ const coin = String(fill.coin);
203
+ const side = String(fill.side);
204
+ const time = finiteNumber(fill.time);
205
+ const notionalUsd = Math.abs(finiteNumber(fill.px) * finiteNumber(fill.sz));
206
+ const previous = clusters.at(-1);
207
+ if (previous && previous.coin === coin && previous.side === side && time - previous.time <= 6e4) {
208
+ previous.time = time;
209
+ previous.notionalUsd += notionalUsd;
210
+ } else {
211
+ clusters.push({ coin, side, time, notionalUsd });
212
+ }
213
+ return clusters;
214
+ }, []);
215
+ const notionals = executions.map((execution) => execution.notionalUsd);
216
+ const totalNotional = notionals.reduce((sum, value) => sum + value, 0);
217
+ const top30Notional = executions.reduce(
218
+ (sum, execution) => sum + (params.top30Symbols.has(execution.coin) ? execution.notionalUsd : 0),
219
+ 0
220
+ );
221
+ const interExecutionMinutes = executions.slice(1).map(
222
+ (execution, index) => (execution.time - executions[index].time) / 6e4
223
+ ).filter((value) => value >= 0);
224
+ const activeDays = new Set(
225
+ fills.map(
226
+ (fill) => new Date(finiteNumber(fill.time)).toISOString().slice(0, 10)
227
+ )
228
+ ).size;
229
+ const rawFillsPerDay = fills.length / days;
230
+ const directionalExecutionsPerDay = executions.length / days;
231
+ const medianNotionalUsd = median(notionals);
232
+ const medianInterExecutionMinutes = median(interExecutionMinutes);
233
+ const top30NotionalShare = totalNotional > 0 ? top30Notional / totalNotional : 0;
234
+ const eligible = params.accountValueUsd >= HYPERLIQUID_WHALE_SELECTION.minimumAccountValueUsd && activeDays >= HYPERLIQUID_WHALE_SELECTION.minimumActiveDays && rawFillsPerDay <= HYPERLIQUID_WHALE_SELECTION.maximumRawFillsPerDay && directionalExecutionsPerDay <= HYPERLIQUID_WHALE_SELECTION.maximumDirectionalExecutionsPerDay && medianNotionalUsd >= HYPERLIQUID_WHALE_SELECTION.minimumMedianNotionalUsd && medianInterExecutionMinutes >= HYPERLIQUID_WHALE_SELECTION.minimumMedianInterExecutionMinutes && top30NotionalShare >= HYPERLIQUID_WHALE_SELECTION.minimumTop30NotionalShare;
235
+ const score = Math.log1p(params.accountValueUsd) * Math.log1p(medianNotionalUsd) * Math.log1p(Math.max(1, medianInterExecutionMinutes)) * Math.sqrt(Math.max(0, activeDays)) * top30NotionalShare / Math.sqrt(1 + rawFillsPerDay);
236
+ return {
237
+ address: params.address.toLowerCase(),
238
+ accountValueUsd: params.accountValueUsd,
239
+ rawFills: fills.length,
240
+ rawFillsPerDay,
241
+ directionalExecutions: executions.length,
242
+ directionalExecutionsPerDay,
243
+ activeDays,
244
+ medianNotionalUsd,
245
+ medianInterExecutionMinutes,
246
+ top30NotionalShare,
247
+ score,
248
+ eligible
249
+ };
250
+ };
251
+ var rankHyperliquidStructuralWhales = (metrics, limit = 100) => metrics.filter((row) => row.eligible).sort(
252
+ (left, right) => right.score - left.score || left.address.localeCompare(right.address)
253
+ ).slice(0, limit);
254
+
255
+ // src/scripts/hyperliquidWhalesUpdate.ts
256
+ var LEADERBOARD_URL = "https://stats-data.hyperliquid.xyz/Mainnet/leaderboard";
257
+ var DAY_MS = 864e5;
258
+ var CANDIDATE_LIMIT = 400;
259
+ import_args.default.option(
260
+ ["t", "to"],
261
+ "Exclusive calibration end (ISO); defaults to the current UTC day"
262
+ );
263
+ import_args.default.option(["d", "days"], "Calibration window in days", 7);
264
+ var flags = import_args.default.parse(process.argv);
265
+ var fingerprint = (addresses) => (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(addresses)).digest("hex").slice(0, 16);
266
+ var runConcurrent = async (params) => {
267
+ let nextIndex = 0;
268
+ await Promise.all(
269
+ Array.from({ length: params.concurrency }, async () => {
270
+ for (; ; ) {
271
+ const index = nextIndex;
272
+ nextIndex += 1;
273
+ if (index >= params.items.length) return;
274
+ await params.worker(params.items[index]);
275
+ }
276
+ })
277
+ );
278
+ };
279
+ var main = async () => {
280
+ const explicitTo = flags.to == null ? Number.NaN : Date.parse(String(flags.to));
281
+ const calibrationToMs = Number.isFinite(explicitTo) ? explicitTo : Math.floor(Date.now() / DAY_MS) * DAY_MS;
282
+ const calibrationDays = Math.max(1, Number(flags.days) || 7);
283
+ const calibrationFromMs = calibrationToMs - calibrationDays * DAY_MS;
284
+ const response = await fetch(LEADERBOARD_URL);
285
+ if (!response.ok) {
286
+ throw new Error(`Hyperliquid leaderboard ${response.status}`);
287
+ }
288
+ const payload = await response.json();
289
+ const candidates = (payload.leaderboardRows ?? []).map((row) => {
290
+ const address = String(row.ethAddress ?? "").toLowerCase();
291
+ const accountValueUsd = Number(row.accountValue);
292
+ const weeklyVolumeUsd = Number(
293
+ row.windowPerformances?.find(([window]) => window === "week")?.[1].vlm
294
+ );
295
+ return {
296
+ address,
297
+ accountValueUsd,
298
+ turnoverToEquity: accountValueUsd > 0 ? weeklyVolumeUsd / accountValueUsd : 0
299
+ };
300
+ }).filter(
301
+ (row) => /^0x[0-9a-f]{40}$/.test(row.address) && row.accountValueUsd >= HYPERLIQUID_WHALE_SELECTION.minimumAccountValueUsd && row.turnoverToEquity >= HYPERLIQUID_WHALE_SELECTION.minimumTurnoverToEquity && row.turnoverToEquity <= HYPERLIQUID_WHALE_SELECTION.maximumTurnoverToEquity
302
+ ).sort(
303
+ (left, right) => left.turnoverToEquity - right.turnoverToEquity || right.accountValueUsd - left.accountValueUsd || left.address.localeCompare(right.address)
304
+ ).slice(0, CANDIDATE_LIMIT);
305
+ const top30Symbols = new Set((0, import_strategies2.getHyperliquidPerpUniverseSnapshot)().symbols);
306
+ const limiter = new HyperliquidInfoRateLimiter();
307
+ const metrics = [];
308
+ let completed = 0;
309
+ await runConcurrent({
310
+ items: candidates,
311
+ concurrency: 2,
312
+ worker: async (candidate) => {
313
+ const result = await fetchHyperliquidUserFillsByTime({
314
+ address: candidate.address,
315
+ startTime: calibrationFromMs,
316
+ endTime: calibrationToMs - 1,
317
+ rateLimiter: limiter,
318
+ maxPages: 1
319
+ });
320
+ metrics.push(
321
+ evaluateHyperliquidWhaleStructure({
322
+ address: candidate.address,
323
+ accountValueUsd: candidate.accountValueUsd,
324
+ fills: result.fills,
325
+ calibrationFromMs,
326
+ calibrationToMs,
327
+ top30Symbols
328
+ })
329
+ );
330
+ completed += 1;
331
+ if (completed % 10 === 0 || completed === candidates.length) {
332
+ console.log(
333
+ `Hyperliquid whale selection ${completed}/${candidates.length}; eligible=${metrics.filter((row) => row.eligible).length}`
334
+ );
335
+ }
336
+ }
337
+ });
338
+ const selected = rankHyperliquidStructuralWhales(metrics, 100);
339
+ if (selected.length !== 100) {
340
+ throw new Error(
341
+ `Only ${selected.length} structural whales passed selection from ${candidates.length} candidates`
342
+ );
343
+ }
344
+ const addresses = selected.map((row) => row.address);
345
+ const timestamp = new Date(calibrationToMs).toISOString();
346
+ const snapshot = {
347
+ schemaVersion: 2,
348
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
349
+ source: "hyperliquid_structural_fills_snapshot",
350
+ selection: {
351
+ calibrationFrom: new Date(calibrationFromMs).toISOString(),
352
+ calibrationTo: timestamp,
353
+ effectiveFrom: timestamp,
354
+ candidateLimit: CANDIDATE_LIMIT,
355
+ ...HYPERLIQUID_WHALE_SELECTION,
356
+ score: "log1p(accountValue)*log1p(medianExecutionNotional)*log1p(max(1,medianInterExecutionMinutes))*sqrt(activeDays)*top30Share/sqrt(1+rawFillsPerDay)",
357
+ forbiddenSelectionMetrics: ["pnl", "roi", "winRate", "closedPnl"]
358
+ },
359
+ fingerprint: fingerprint(addresses),
360
+ size: 100,
361
+ addresses
362
+ };
363
+ console.log(JSON.stringify(snapshot, null, 2));
364
+ return snapshot;
365
+ };
366
+ // Annotate the CommonJS export names for ESM import in node:
367
+ 0 && (module.exports = {
368
+ main
369
+ });