@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.
@@ -5185,6 +5185,412 @@ var backfillCoinMarketCapContextForBacktest = backfillCoinMarketCapContext;
5185
5185
  var backfillCoinMarketCapContextForReplay = backfillCoinMarketCapContext;
5186
5186
  var backfillCoinMarketCapContextForSignals = (params) => backfillCoinMarketCapContext({ ...params, refreshStaleFearGreed: true });
5187
5187
 
5188
+ // src/lib/hyperliquidWhaleBackfill.ts
5189
+ var import_timescale4 = require("@tradejs/infra/timescale");
5190
+ var import_strategies2 = require("@tradejs/node/strategies");
5191
+
5192
+ // src/lib/hyperliquidWhaleData.ts
5193
+ var HYPERLIQUID_WHALE_BUCKET_MS = 6e4;
5194
+ var finiteNumber = (value) => {
5195
+ const numeric = typeof value === "number" ? value : Number(value);
5196
+ return Number.isFinite(numeric) ? numeric : null;
5197
+ };
5198
+ var POSITION_EPSILON = 1e-12;
5199
+ var normalizePosition = (value) => Math.abs(value) <= POSITION_EPSILON ? 0 : value;
5200
+ var classifyHyperliquidPositionChange = (params) => {
5201
+ const startValue = finiteNumber(params.startPosition);
5202
+ const size = finiteNumber(params.size);
5203
+ const side = String(params.side ?? "").toUpperCase();
5204
+ if (startValue == null || size == null || size <= 0) return null;
5205
+ if (side !== "B" && side !== "A") return null;
5206
+ const startPosition = normalizePosition(startValue);
5207
+ const endPosition = normalizePosition(
5208
+ startPosition + (side === "B" ? size : -size)
5209
+ );
5210
+ const startLong = Math.max(startPosition, 0);
5211
+ const endLong = Math.max(endPosition, 0);
5212
+ const startShort = Math.max(-startPosition, 0);
5213
+ const endShort = Math.max(-endPosition, 0);
5214
+ const longEntrySize = Math.max(0, endLong - startLong);
5215
+ const shortEntrySize = Math.max(0, endShort - startShort);
5216
+ const longExitSize = Math.max(0, startLong - endLong);
5217
+ const shortExitSize = Math.max(0, startShort - endShort);
5218
+ const flipped = startPosition * endPosition < 0;
5219
+ const opened = startPosition === 0;
5220
+ const closed = endPosition === 0;
5221
+ const increased = Math.sign(startPosition) === Math.sign(endPosition) && Math.abs(endPosition) > Math.abs(startPosition);
5222
+ return {
5223
+ startPosition,
5224
+ endPosition,
5225
+ action: flipped ? "flip" : opened ? "open" : closed ? "close" : increased ? "increase" : "reduce",
5226
+ longEntrySize,
5227
+ shortEntrySize,
5228
+ longExitSize,
5229
+ shortExitSize
5230
+ };
5231
+ };
5232
+ var normalizeAddress = (value) => typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value) ? value.toLowerCase() : null;
5233
+ var normalizeTradeFields = (value) => {
5234
+ const symbol = typeof value.coin === "string" ? value.coin.trim() : "";
5235
+ const price = finiteNumber(value.px);
5236
+ const size = finiteNumber(value.sz);
5237
+ const timestamp = finiteNumber(value.time);
5238
+ const tid = typeof value.tid === "string" || typeof value.tid === "number" ? String(value.tid) : "";
5239
+ if (!symbol || price == null || size == null || timestamp == null || !tid || price <= 0 || size <= 0) {
5240
+ return null;
5241
+ }
5242
+ return {
5243
+ symbol,
5244
+ price,
5245
+ size,
5246
+ timestamp,
5247
+ tid,
5248
+ notionalUsd: price * size
5249
+ };
5250
+ };
5251
+ var normalizeHyperliquidUserFill = (params) => {
5252
+ const normalized = normalizeTradeFields(params.fill);
5253
+ if (!normalized || !params.trackedSymbols.has(normalized.symbol)) return null;
5254
+ const address = normalizeAddress(params.address);
5255
+ if (!address) return null;
5256
+ const side = String(params.fill.side ?? "").toUpperCase();
5257
+ if (side !== "B" && side !== "A") return null;
5258
+ const positionChange = classifyHyperliquidPositionChange({
5259
+ startPosition: params.fill.startPosition,
5260
+ side,
5261
+ size: normalized.size
5262
+ });
5263
+ const closedPnl = finiteNumber(params.fill.closedPnl);
5264
+ const liquidation = params.fill.liquidation != null && typeof params.fill.liquidation === "object";
5265
+ const positionFields = positionChange ? side === "B" ? {
5266
+ buyerStartPosition: positionChange.startPosition,
5267
+ buyerEndPosition: positionChange.endPosition,
5268
+ buyerPositionAction: positionChange.action,
5269
+ buyerClosedPnl: closedPnl,
5270
+ buyerLiquidation: liquidation
5271
+ } : {
5272
+ sellerStartPosition: positionChange.startPosition,
5273
+ sellerEndPosition: positionChange.endPosition,
5274
+ sellerPositionAction: positionChange.action,
5275
+ sellerClosedPnl: closedPnl,
5276
+ sellerLiquidation: liquidation
5277
+ } : {};
5278
+ return {
5279
+ symbol: normalized.symbol,
5280
+ ts: new Date(normalized.timestamp),
5281
+ tid: normalized.tid,
5282
+ price: normalized.price,
5283
+ size: normalized.size,
5284
+ notionalUsd: normalized.notionalUsd,
5285
+ buyerAddress: side === "B" ? address : null,
5286
+ sellerAddress: side === "A" ? address : null,
5287
+ buyerTracked: side === "B",
5288
+ sellerTracked: side === "A",
5289
+ ...positionFields,
5290
+ ...params.identity,
5291
+ source: "hyperliquid_user_fills"
5292
+ };
5293
+ };
5294
+
5295
+ // src/lib/hyperliquidRateLimiter.ts
5296
+ var DEFAULT_WEIGHT_PER_MINUTE = 1e3;
5297
+ var WINDOW_MS = 6e4;
5298
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
5299
+ var HyperliquidInfoRateLimiter = class {
5300
+ constructor(capacity = DEFAULT_WEIGHT_PER_MINUTE, now = Date.now, wait = sleep2) {
5301
+ this.capacity = capacity;
5302
+ this.now = now;
5303
+ this.wait = wait;
5304
+ this.reservations = [];
5305
+ this.queue = Promise.resolve();
5306
+ }
5307
+ reserve(weight) {
5308
+ const normalizedWeight = Math.max(1, Math.ceil(weight));
5309
+ const pending = this.queue.then(
5310
+ () => this.reserveInternal(normalizedWeight)
5311
+ );
5312
+ this.queue = pending.catch(() => void 0);
5313
+ return pending;
5314
+ }
5315
+ async reserveInternal(weight) {
5316
+ if (weight > this.capacity) {
5317
+ throw new Error(
5318
+ `Hyperliquid request weight ${weight} exceeds limiter capacity ${this.capacity}`
5319
+ );
5320
+ }
5321
+ for (; ; ) {
5322
+ const nowMs = this.now();
5323
+ this.reservations = this.reservations.filter(
5324
+ (reservation) => reservation.atMs > nowMs - WINDOW_MS
5325
+ );
5326
+ const used = this.reservations.reduce(
5327
+ (sum, reservation) => sum + reservation.weight,
5328
+ 0
5329
+ );
5330
+ if (used + weight <= this.capacity) {
5331
+ this.reservations.push({ atMs: nowMs, weight });
5332
+ return;
5333
+ }
5334
+ const oldest = this.reservations[0];
5335
+ await this.wait(Math.max(1, oldest.atMs + WINDOW_MS - nowMs));
5336
+ }
5337
+ }
5338
+ };
5339
+ var hyperliquidInfoResponseWeight = (rows) => Math.max(0, Math.ceil(Math.max(0, rows) / 20));
5340
+
5341
+ // src/lib/hyperliquidWhaleBackfill.ts
5342
+ var DEFAULT_BASE_URL = "https://api.hyperliquid.xyz";
5343
+ var DEFAULT_CONCURRENCY = 2;
5344
+ var DEFAULT_RATE_LIMIT_WEIGHT = 1e3;
5345
+ var MAX_PAGE_SIZE = 2e3;
5346
+ var MAX_PAGES_PER_WHALE = 5;
5347
+ var REQUEST_WEIGHT = 20;
5348
+ var sleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
5349
+ var positiveInt = (value, fallback) => {
5350
+ const parsed = Number.parseInt(String(value ?? ""), 10);
5351
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
5352
+ };
5353
+ var minuteCeil = (value) => Math.ceil(value / HYPERLIQUID_WHALE_BUCKET_MS) * HYPERLIQUID_WHALE_BUCKET_MS;
5354
+ var nextMinute = (value) => (Math.floor(value / HYPERLIQUID_WHALE_BUCKET_MS) + 1) * HYPERLIQUID_WHALE_BUCKET_MS;
5355
+ var fetchHyperliquidUserFillsByTime = async (params) => {
5356
+ const fetchImpl = params.fetchImpl ?? fetch;
5357
+ const limiter = params.rateLimiter ?? new HyperliquidInfoRateLimiter(
5358
+ positiveInt(
5359
+ process.env.HYPERLIQUID_WHALE_RATE_LIMIT_WEIGHT,
5360
+ DEFAULT_RATE_LIMIT_WEIGHT
5361
+ ),
5362
+ Date.now,
5363
+ params.wait ?? sleep3
5364
+ );
5365
+ const fills = [];
5366
+ let cursor = params.startTime;
5367
+ for (let page = 0; page < (params.maxPages ?? MAX_PAGES_PER_WHALE) && cursor <= params.endTime; page += 1) {
5368
+ await limiter.reserve(REQUEST_WEIGHT);
5369
+ const response = await fetchImpl(
5370
+ `${process.env.HYPERLIQUID_API_BASE_URL?.trim() || DEFAULT_BASE_URL}/info`,
5371
+ {
5372
+ method: "POST",
5373
+ headers: { "content-type": "application/json" },
5374
+ body: JSON.stringify({
5375
+ type: "userFillsByTime",
5376
+ user: params.address,
5377
+ startTime: cursor,
5378
+ endTime: params.endTime,
5379
+ aggregateByTime: true
5380
+ })
5381
+ }
5382
+ );
5383
+ if (!response.ok) {
5384
+ throw new Error(
5385
+ `Hyperliquid userFillsByTime ${response.status}: ${await response.text()}`
5386
+ );
5387
+ }
5388
+ const raw = await response.json();
5389
+ const pageRows = Array.isArray(raw) ? raw : [];
5390
+ const responseWeight = hyperliquidInfoResponseWeight(pageRows.length);
5391
+ if (responseWeight > 0) await limiter.reserve(responseWeight);
5392
+ fills.push(...pageRows);
5393
+ if (pageRows.length < MAX_PAGE_SIZE) {
5394
+ return { fills, truncated: false, coveredFromMs: params.startTime };
5395
+ }
5396
+ const lastTime = Math.max(
5397
+ ...pageRows.map((row) => Number(row.time)).filter(Number.isFinite)
5398
+ );
5399
+ if (!Number.isFinite(lastTime) || lastTime < cursor) {
5400
+ throw new Error(
5401
+ `Hyperliquid fills pagination stalled for ${params.address}`
5402
+ );
5403
+ }
5404
+ cursor = lastTime + 1;
5405
+ }
5406
+ if (cursor > params.endTime) {
5407
+ return { fills, truncated: false, coveredFromMs: params.startTime };
5408
+ }
5409
+ const firstAvailableTime = Math.min(
5410
+ ...fills.map((fill) => Number(fill.time)).filter(Number.isFinite)
5411
+ );
5412
+ return {
5413
+ fills,
5414
+ truncated: true,
5415
+ coveredFromMs: Math.min(params.endTime + 1, nextMinute(firstAvailableTime))
5416
+ };
5417
+ };
5418
+ var runConcurrent = async (params) => {
5419
+ let nextIndex = 0;
5420
+ await Promise.all(
5421
+ Array.from(
5422
+ { length: Math.min(params.concurrency, params.items.length) },
5423
+ async () => {
5424
+ for (; ; ) {
5425
+ const index = nextIndex;
5426
+ nextIndex += 1;
5427
+ if (index >= params.items.length) return;
5428
+ await params.worker(params.items[index], index);
5429
+ }
5430
+ }
5431
+ )
5432
+ );
5433
+ };
5434
+ var backfillHyperliquidWhaleContext = async (params) => {
5435
+ const universe = (0, import_strategies2.getHyperliquidPerpUniverseSnapshot)();
5436
+ const whales = (0, import_strategies2.getHyperliquidWhaleRegistrySnapshot)();
5437
+ const trackedSymbols = new Set(universe.symbols);
5438
+ const identity = {
5439
+ universeFingerprint: universe.fingerprint,
5440
+ whaleRegistryFingerprint: whales.fingerprint
5441
+ };
5442
+ const fromMs = Math.floor(params.startMs / HYPERLIQUID_WHALE_BUCKET_MS) * HYPERLIQUID_WHALE_BUCKET_MS;
5443
+ const toMs = minuteCeil(params.endMs);
5444
+ const cached = await (0, import_timescale4.hasHyperliquidWhaleBackfillCoverage)({
5445
+ fromMs,
5446
+ toMs,
5447
+ ...identity
5448
+ });
5449
+ const emptyResult = () => ({
5450
+ cached,
5451
+ cacheOnly: params.cacheOnly,
5452
+ fills: 0,
5453
+ events: 0,
5454
+ buckets: 0,
5455
+ coverageBuckets: 0,
5456
+ complete: cached,
5457
+ walletsProcessed: 0,
5458
+ walletsCached: 0,
5459
+ completeWallets: 0,
5460
+ truncatedWallets: 0,
5461
+ failedWallets: 0,
5462
+ fromMs,
5463
+ toMs
5464
+ });
5465
+ if (cached || params.cacheOnly) return emptyResult();
5466
+ const limiter = new HyperliquidInfoRateLimiter(
5467
+ positiveInt(
5468
+ process.env.HYPERLIQUID_WHALE_RATE_LIMIT_WEIGHT,
5469
+ DEFAULT_RATE_LIMIT_WEIGHT
5470
+ ),
5471
+ Date.now,
5472
+ params.wait ?? sleep3
5473
+ );
5474
+ const concurrency = Math.min(
5475
+ 4,
5476
+ positiveInt(
5477
+ params.concurrency ?? process.env.HYPERLIQUID_WHALE_CONCURRENCY,
5478
+ DEFAULT_CONCURRENCY
5479
+ )
5480
+ );
5481
+ let fills = 0;
5482
+ let events = 0;
5483
+ let walletsProcessed = 0;
5484
+ let walletsCached = 0;
5485
+ let completeWallets = 0;
5486
+ let truncatedWallets = 0;
5487
+ let failedWallets = 0;
5488
+ const failures = [];
5489
+ await runConcurrent({
5490
+ items: whales.addresses,
5491
+ concurrency,
5492
+ worker: async (address) => {
5493
+ const previous = await (0, import_timescale4.getHyperliquidWhaleWalletCoverage)({
5494
+ address,
5495
+ fromMs,
5496
+ toMs,
5497
+ ...identity
5498
+ });
5499
+ if (previous && previous.status !== "failed") {
5500
+ walletsCached += 1;
5501
+ if (previous.status === "complete") completeWallets += 1;
5502
+ else truncatedWallets += 1;
5503
+ } else {
5504
+ try {
5505
+ const result2 = await fetchHyperliquidUserFillsByTime({
5506
+ address,
5507
+ startTime: fromMs,
5508
+ endTime: toMs - 1,
5509
+ fetchImpl: params.fetchImpl,
5510
+ rateLimiter: limiter
5511
+ });
5512
+ const addressEvents = result2.fills.map(
5513
+ (fill) => normalizeHyperliquidUserFill({
5514
+ fill,
5515
+ address,
5516
+ trackedSymbols,
5517
+ identity
5518
+ })
5519
+ ).filter((row) => row != null);
5520
+ await (0, import_timescale4.upsertHyperliquidWhaleTradeEvents)(addressEvents);
5521
+ await (0, import_timescale4.upsertHyperliquidWhaleWalletCoverage)({
5522
+ address,
5523
+ fromMs,
5524
+ toMs,
5525
+ coveredFromMs: result2.coveredFromMs,
5526
+ coveredToMs: toMs,
5527
+ status: result2.truncated ? "truncated" : "complete",
5528
+ fillsCount: result2.fills.length,
5529
+ ...identity
5530
+ });
5531
+ fills += result2.fills.length;
5532
+ events += addressEvents.length;
5533
+ if (result2.truncated) truncatedWallets += 1;
5534
+ else completeWallets += 1;
5535
+ } catch (error) {
5536
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
5537
+ failures.push(normalizedError);
5538
+ failedWallets += 1;
5539
+ await (0, import_timescale4.upsertHyperliquidWhaleWalletCoverage)({
5540
+ address,
5541
+ fromMs,
5542
+ toMs,
5543
+ coveredFromMs: null,
5544
+ coveredToMs: null,
5545
+ status: "failed",
5546
+ fillsCount: 0,
5547
+ error: normalizedError.message,
5548
+ ...identity
5549
+ });
5550
+ }
5551
+ }
5552
+ walletsProcessed += 1;
5553
+ params.log?.(
5554
+ `Hyperliquid whales ${walletsProcessed}/${whales.addresses.length}: complete=${completeWallets}, truncated=${truncatedWallets}, failed=${failedWallets}, fills=${fills}, events=${events}`
5555
+ );
5556
+ }
5557
+ });
5558
+ const buckets = await (0, import_timescale4.rebuildHyperliquidWhaleFlowRows)({
5559
+ fromMs,
5560
+ toMs,
5561
+ ...identity
5562
+ });
5563
+ const coverageBuckets = await (0, import_timescale4.rebuildHyperliquidWhaleCoverageRows)({
5564
+ fromMs,
5565
+ toMs,
5566
+ expectedWhales: whales.addresses.length,
5567
+ ...identity
5568
+ });
5569
+ const result = {
5570
+ cached: false,
5571
+ cacheOnly: false,
5572
+ fills,
5573
+ events,
5574
+ buckets,
5575
+ coverageBuckets,
5576
+ complete: truncatedWallets === 0 && failedWallets === 0,
5577
+ walletsProcessed,
5578
+ walletsCached,
5579
+ completeWallets,
5580
+ truncatedWallets,
5581
+ failedWallets,
5582
+ fromMs,
5583
+ toMs
5584
+ };
5585
+ if (params.strict !== false && failures.length) {
5586
+ throw new AggregateError(
5587
+ failures,
5588
+ `Hyperliquid whale backfill failed for ${failures.length}/${whales.addresses.length} wallets`
5589
+ );
5590
+ }
5591
+ return result;
5592
+ };
5593
+
5188
5594
  // src/lib/marketContextPrepare.ts
5189
5595
  var shouldPrepareDerivativesContextForRun = (params) => {
5190
5596
  if (params.universe === "tradfi") return false;
@@ -5235,6 +5641,20 @@ var shouldPrepareCoinMarketCapContextForRun = (params) => {
5235
5641
  cacheOnly: params.cacheOnly
5236
5642
  });
5237
5643
  };
5644
+ var shouldPrepareHyperliquidWhaleContextForRun = (params) => {
5645
+ const strategyRequiresContext = params.strategyNames?.includes(
5646
+ "HyperliquidConsensus"
5647
+ );
5648
+ if (params.universe === "tradfi" || params.mode === "signals" || params.cacheOnly || !params.aiEnabled && !params.mlEnabled && !strategyRequiresContext) {
5649
+ return false;
5650
+ }
5651
+ const configured = String(
5652
+ process.env.HYPERLIQUID_WHALE_BACKFILL_ENABLED ?? ""
5653
+ ).trim().toLowerCase();
5654
+ if (["0", "false", "no", "off"].includes(configured)) return false;
5655
+ if (strategyRequiresContext) return true;
5656
+ return ["1", "true", "yes", "on"].includes(configured);
5657
+ };
5238
5658
  var resolveCoinMarketCapBackfillForMode = (mode) => mode === "backtest" ? backfillCoinMarketCapContextForBacktest : mode === "signals" ? backfillCoinMarketCapContextForSignals : backfillCoinMarketCapContextForReplay;
5239
5659
  var buildCoinMarketCapBackfillParams = (params) => ({
5240
5660
  userName: params.userName,
@@ -5271,6 +5691,18 @@ var prepareMarketContextForRun = async (params) => {
5271
5691
  )
5272
5692
  );
5273
5693
  }
5694
+ if (shouldPrepareHyperliquidWhaleContextForRun(params)) {
5695
+ await timeOperation2(
5696
+ "hyperliquid whale context backfill",
5697
+ () => backfillHyperliquidWhaleContext({
5698
+ startMs: params.preloadStartMs ?? params.startMs,
5699
+ endMs: params.endMs,
5700
+ cacheOnly: params.cacheOnly,
5701
+ strict: false,
5702
+ log
5703
+ })
5704
+ );
5705
+ }
5274
5706
  if (shouldPrepareBinanceMarketContextForRun(params)) {
5275
5707
  await timeOperation2("binance market context backfill", () => {
5276
5708
  const backfill = params.mode === "backtest" ? backfillBinanceMarketContextForBacktest : params.mode === "signals" ? backfillBinanceMarketContextForSignals : backfillBinanceMarketContextForReplay;
@@ -6963,6 +7395,7 @@ var runtimeParity = async () => {
6963
7395
  cacheOnly: Boolean(flags.cacheOnly),
6964
7396
  aiEnabled: hasParityAiTargets,
6965
7397
  mlEnabled: hasParityMlTargets,
7398
+ strategyNames: replayTargets.map((target) => target.strategy),
6966
7399
  log: (message) => console.log(import_chalk6.default.gray(message))
6967
7400
  });
6968
7401
  if (!flags.cacheOnly) {