@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.
@@ -4539,6 +4539,412 @@ var backfillCoinMarketCapContextForBacktest = backfillCoinMarketCapContext;
4539
4539
  var backfillCoinMarketCapContextForReplay = backfillCoinMarketCapContext;
4540
4540
  var backfillCoinMarketCapContextForSignals = (params) => backfillCoinMarketCapContext({ ...params, refreshStaleFearGreed: true });
4541
4541
 
4542
+ // src/lib/hyperliquidWhaleBackfill.ts
4543
+ var import_timescale4 = require("@tradejs/infra/timescale");
4544
+ var import_strategies2 = require("@tradejs/node/strategies");
4545
+
4546
+ // src/lib/hyperliquidWhaleData.ts
4547
+ var HYPERLIQUID_WHALE_BUCKET_MS = 6e4;
4548
+ var finiteNumber = (value) => {
4549
+ const numeric = typeof value === "number" ? value : Number(value);
4550
+ return Number.isFinite(numeric) ? numeric : null;
4551
+ };
4552
+ var POSITION_EPSILON = 1e-12;
4553
+ var normalizePosition = (value) => Math.abs(value) <= POSITION_EPSILON ? 0 : value;
4554
+ var classifyHyperliquidPositionChange = (params) => {
4555
+ const startValue = finiteNumber(params.startPosition);
4556
+ const size = finiteNumber(params.size);
4557
+ const side = String(params.side ?? "").toUpperCase();
4558
+ if (startValue == null || size == null || size <= 0) return null;
4559
+ if (side !== "B" && side !== "A") return null;
4560
+ const startPosition = normalizePosition(startValue);
4561
+ const endPosition = normalizePosition(
4562
+ startPosition + (side === "B" ? size : -size)
4563
+ );
4564
+ const startLong = Math.max(startPosition, 0);
4565
+ const endLong = Math.max(endPosition, 0);
4566
+ const startShort = Math.max(-startPosition, 0);
4567
+ const endShort = Math.max(-endPosition, 0);
4568
+ const longEntrySize = Math.max(0, endLong - startLong);
4569
+ const shortEntrySize = Math.max(0, endShort - startShort);
4570
+ const longExitSize = Math.max(0, startLong - endLong);
4571
+ const shortExitSize = Math.max(0, startShort - endShort);
4572
+ const flipped = startPosition * endPosition < 0;
4573
+ const opened = startPosition === 0;
4574
+ const closed = endPosition === 0;
4575
+ const increased = Math.sign(startPosition) === Math.sign(endPosition) && Math.abs(endPosition) > Math.abs(startPosition);
4576
+ return {
4577
+ startPosition,
4578
+ endPosition,
4579
+ action: flipped ? "flip" : opened ? "open" : closed ? "close" : increased ? "increase" : "reduce",
4580
+ longEntrySize,
4581
+ shortEntrySize,
4582
+ longExitSize,
4583
+ shortExitSize
4584
+ };
4585
+ };
4586
+ var normalizeAddress = (value) => typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value) ? value.toLowerCase() : null;
4587
+ var normalizeTradeFields = (value) => {
4588
+ const symbol = typeof value.coin === "string" ? value.coin.trim() : "";
4589
+ const price = finiteNumber(value.px);
4590
+ const size = finiteNumber(value.sz);
4591
+ const timestamp = finiteNumber(value.time);
4592
+ const tid = typeof value.tid === "string" || typeof value.tid === "number" ? String(value.tid) : "";
4593
+ if (!symbol || price == null || size == null || timestamp == null || !tid || price <= 0 || size <= 0) {
4594
+ return null;
4595
+ }
4596
+ return {
4597
+ symbol,
4598
+ price,
4599
+ size,
4600
+ timestamp,
4601
+ tid,
4602
+ notionalUsd: price * size
4603
+ };
4604
+ };
4605
+ var normalizeHyperliquidUserFill = (params) => {
4606
+ const normalized = normalizeTradeFields(params.fill);
4607
+ if (!normalized || !params.trackedSymbols.has(normalized.symbol)) return null;
4608
+ const address = normalizeAddress(params.address);
4609
+ if (!address) return null;
4610
+ const side = String(params.fill.side ?? "").toUpperCase();
4611
+ if (side !== "B" && side !== "A") return null;
4612
+ const positionChange = classifyHyperliquidPositionChange({
4613
+ startPosition: params.fill.startPosition,
4614
+ side,
4615
+ size: normalized.size
4616
+ });
4617
+ const closedPnl = finiteNumber(params.fill.closedPnl);
4618
+ const liquidation = params.fill.liquidation != null && typeof params.fill.liquidation === "object";
4619
+ const positionFields = positionChange ? side === "B" ? {
4620
+ buyerStartPosition: positionChange.startPosition,
4621
+ buyerEndPosition: positionChange.endPosition,
4622
+ buyerPositionAction: positionChange.action,
4623
+ buyerClosedPnl: closedPnl,
4624
+ buyerLiquidation: liquidation
4625
+ } : {
4626
+ sellerStartPosition: positionChange.startPosition,
4627
+ sellerEndPosition: positionChange.endPosition,
4628
+ sellerPositionAction: positionChange.action,
4629
+ sellerClosedPnl: closedPnl,
4630
+ sellerLiquidation: liquidation
4631
+ } : {};
4632
+ return {
4633
+ symbol: normalized.symbol,
4634
+ ts: new Date(normalized.timestamp),
4635
+ tid: normalized.tid,
4636
+ price: normalized.price,
4637
+ size: normalized.size,
4638
+ notionalUsd: normalized.notionalUsd,
4639
+ buyerAddress: side === "B" ? address : null,
4640
+ sellerAddress: side === "A" ? address : null,
4641
+ buyerTracked: side === "B",
4642
+ sellerTracked: side === "A",
4643
+ ...positionFields,
4644
+ ...params.identity,
4645
+ source: "hyperliquid_user_fills"
4646
+ };
4647
+ };
4648
+
4649
+ // src/lib/hyperliquidRateLimiter.ts
4650
+ var DEFAULT_WEIGHT_PER_MINUTE = 1e3;
4651
+ var WINDOW_MS = 6e4;
4652
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
4653
+ var HyperliquidInfoRateLimiter = class {
4654
+ constructor(capacity = DEFAULT_WEIGHT_PER_MINUTE, now = Date.now, wait = sleep2) {
4655
+ this.capacity = capacity;
4656
+ this.now = now;
4657
+ this.wait = wait;
4658
+ this.reservations = [];
4659
+ this.queue = Promise.resolve();
4660
+ }
4661
+ reserve(weight) {
4662
+ const normalizedWeight = Math.max(1, Math.ceil(weight));
4663
+ const pending = this.queue.then(
4664
+ () => this.reserveInternal(normalizedWeight)
4665
+ );
4666
+ this.queue = pending.catch(() => void 0);
4667
+ return pending;
4668
+ }
4669
+ async reserveInternal(weight) {
4670
+ if (weight > this.capacity) {
4671
+ throw new Error(
4672
+ `Hyperliquid request weight ${weight} exceeds limiter capacity ${this.capacity}`
4673
+ );
4674
+ }
4675
+ for (; ; ) {
4676
+ const nowMs = this.now();
4677
+ this.reservations = this.reservations.filter(
4678
+ (reservation) => reservation.atMs > nowMs - WINDOW_MS
4679
+ );
4680
+ const used = this.reservations.reduce(
4681
+ (sum, reservation) => sum + reservation.weight,
4682
+ 0
4683
+ );
4684
+ if (used + weight <= this.capacity) {
4685
+ this.reservations.push({ atMs: nowMs, weight });
4686
+ return;
4687
+ }
4688
+ const oldest = this.reservations[0];
4689
+ await this.wait(Math.max(1, oldest.atMs + WINDOW_MS - nowMs));
4690
+ }
4691
+ }
4692
+ };
4693
+ var hyperliquidInfoResponseWeight = (rows) => Math.max(0, Math.ceil(Math.max(0, rows) / 20));
4694
+
4695
+ // src/lib/hyperliquidWhaleBackfill.ts
4696
+ var DEFAULT_BASE_URL = "https://api.hyperliquid.xyz";
4697
+ var DEFAULT_CONCURRENCY = 2;
4698
+ var DEFAULT_RATE_LIMIT_WEIGHT = 1e3;
4699
+ var MAX_PAGE_SIZE = 2e3;
4700
+ var MAX_PAGES_PER_WHALE = 5;
4701
+ var REQUEST_WEIGHT = 20;
4702
+ var sleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
4703
+ var positiveInt = (value, fallback) => {
4704
+ const parsed = Number.parseInt(String(value ?? ""), 10);
4705
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
4706
+ };
4707
+ var minuteCeil = (value) => Math.ceil(value / HYPERLIQUID_WHALE_BUCKET_MS) * HYPERLIQUID_WHALE_BUCKET_MS;
4708
+ var nextMinute = (value) => (Math.floor(value / HYPERLIQUID_WHALE_BUCKET_MS) + 1) * HYPERLIQUID_WHALE_BUCKET_MS;
4709
+ var fetchHyperliquidUserFillsByTime = async (params) => {
4710
+ const fetchImpl = params.fetchImpl ?? fetch;
4711
+ const limiter = params.rateLimiter ?? new HyperliquidInfoRateLimiter(
4712
+ positiveInt(
4713
+ process.env.HYPERLIQUID_WHALE_RATE_LIMIT_WEIGHT,
4714
+ DEFAULT_RATE_LIMIT_WEIGHT
4715
+ ),
4716
+ Date.now,
4717
+ params.wait ?? sleep3
4718
+ );
4719
+ const fills = [];
4720
+ let cursor = params.startTime;
4721
+ for (let page = 0; page < (params.maxPages ?? MAX_PAGES_PER_WHALE) && cursor <= params.endTime; page += 1) {
4722
+ await limiter.reserve(REQUEST_WEIGHT);
4723
+ const response = await fetchImpl(
4724
+ `${process.env.HYPERLIQUID_API_BASE_URL?.trim() || DEFAULT_BASE_URL}/info`,
4725
+ {
4726
+ method: "POST",
4727
+ headers: { "content-type": "application/json" },
4728
+ body: JSON.stringify({
4729
+ type: "userFillsByTime",
4730
+ user: params.address,
4731
+ startTime: cursor,
4732
+ endTime: params.endTime,
4733
+ aggregateByTime: true
4734
+ })
4735
+ }
4736
+ );
4737
+ if (!response.ok) {
4738
+ throw new Error(
4739
+ `Hyperliquid userFillsByTime ${response.status}: ${await response.text()}`
4740
+ );
4741
+ }
4742
+ const raw = await response.json();
4743
+ const pageRows = Array.isArray(raw) ? raw : [];
4744
+ const responseWeight = hyperliquidInfoResponseWeight(pageRows.length);
4745
+ if (responseWeight > 0) await limiter.reserve(responseWeight);
4746
+ fills.push(...pageRows);
4747
+ if (pageRows.length < MAX_PAGE_SIZE) {
4748
+ return { fills, truncated: false, coveredFromMs: params.startTime };
4749
+ }
4750
+ const lastTime = Math.max(
4751
+ ...pageRows.map((row) => Number(row.time)).filter(Number.isFinite)
4752
+ );
4753
+ if (!Number.isFinite(lastTime) || lastTime < cursor) {
4754
+ throw new Error(
4755
+ `Hyperliquid fills pagination stalled for ${params.address}`
4756
+ );
4757
+ }
4758
+ cursor = lastTime + 1;
4759
+ }
4760
+ if (cursor > params.endTime) {
4761
+ return { fills, truncated: false, coveredFromMs: params.startTime };
4762
+ }
4763
+ const firstAvailableTime = Math.min(
4764
+ ...fills.map((fill) => Number(fill.time)).filter(Number.isFinite)
4765
+ );
4766
+ return {
4767
+ fills,
4768
+ truncated: true,
4769
+ coveredFromMs: Math.min(params.endTime + 1, nextMinute(firstAvailableTime))
4770
+ };
4771
+ };
4772
+ var runConcurrent = async (params) => {
4773
+ let nextIndex = 0;
4774
+ await Promise.all(
4775
+ Array.from(
4776
+ { length: Math.min(params.concurrency, params.items.length) },
4777
+ async () => {
4778
+ for (; ; ) {
4779
+ const index = nextIndex;
4780
+ nextIndex += 1;
4781
+ if (index >= params.items.length) return;
4782
+ await params.worker(params.items[index], index);
4783
+ }
4784
+ }
4785
+ )
4786
+ );
4787
+ };
4788
+ var backfillHyperliquidWhaleContext = async (params) => {
4789
+ const universe = (0, import_strategies2.getHyperliquidPerpUniverseSnapshot)();
4790
+ const whales = (0, import_strategies2.getHyperliquidWhaleRegistrySnapshot)();
4791
+ const trackedSymbols = new Set(universe.symbols);
4792
+ const identity = {
4793
+ universeFingerprint: universe.fingerprint,
4794
+ whaleRegistryFingerprint: whales.fingerprint
4795
+ };
4796
+ const fromMs = Math.floor(params.startMs / HYPERLIQUID_WHALE_BUCKET_MS) * HYPERLIQUID_WHALE_BUCKET_MS;
4797
+ const toMs = minuteCeil(params.endMs);
4798
+ const cached = await (0, import_timescale4.hasHyperliquidWhaleBackfillCoverage)({
4799
+ fromMs,
4800
+ toMs,
4801
+ ...identity
4802
+ });
4803
+ const emptyResult = () => ({
4804
+ cached,
4805
+ cacheOnly: params.cacheOnly,
4806
+ fills: 0,
4807
+ events: 0,
4808
+ buckets: 0,
4809
+ coverageBuckets: 0,
4810
+ complete: cached,
4811
+ walletsProcessed: 0,
4812
+ walletsCached: 0,
4813
+ completeWallets: 0,
4814
+ truncatedWallets: 0,
4815
+ failedWallets: 0,
4816
+ fromMs,
4817
+ toMs
4818
+ });
4819
+ if (cached || params.cacheOnly) return emptyResult();
4820
+ const limiter = new HyperliquidInfoRateLimiter(
4821
+ positiveInt(
4822
+ process.env.HYPERLIQUID_WHALE_RATE_LIMIT_WEIGHT,
4823
+ DEFAULT_RATE_LIMIT_WEIGHT
4824
+ ),
4825
+ Date.now,
4826
+ params.wait ?? sleep3
4827
+ );
4828
+ const concurrency = Math.min(
4829
+ 4,
4830
+ positiveInt(
4831
+ params.concurrency ?? process.env.HYPERLIQUID_WHALE_CONCURRENCY,
4832
+ DEFAULT_CONCURRENCY
4833
+ )
4834
+ );
4835
+ let fills = 0;
4836
+ let events = 0;
4837
+ let walletsProcessed = 0;
4838
+ let walletsCached = 0;
4839
+ let completeWallets = 0;
4840
+ let truncatedWallets = 0;
4841
+ let failedWallets = 0;
4842
+ const failures = [];
4843
+ await runConcurrent({
4844
+ items: whales.addresses,
4845
+ concurrency,
4846
+ worker: async (address) => {
4847
+ const previous = await (0, import_timescale4.getHyperliquidWhaleWalletCoverage)({
4848
+ address,
4849
+ fromMs,
4850
+ toMs,
4851
+ ...identity
4852
+ });
4853
+ if (previous && previous.status !== "failed") {
4854
+ walletsCached += 1;
4855
+ if (previous.status === "complete") completeWallets += 1;
4856
+ else truncatedWallets += 1;
4857
+ } else {
4858
+ try {
4859
+ const result2 = await fetchHyperliquidUserFillsByTime({
4860
+ address,
4861
+ startTime: fromMs,
4862
+ endTime: toMs - 1,
4863
+ fetchImpl: params.fetchImpl,
4864
+ rateLimiter: limiter
4865
+ });
4866
+ const addressEvents = result2.fills.map(
4867
+ (fill) => normalizeHyperliquidUserFill({
4868
+ fill,
4869
+ address,
4870
+ trackedSymbols,
4871
+ identity
4872
+ })
4873
+ ).filter((row) => row != null);
4874
+ await (0, import_timescale4.upsertHyperliquidWhaleTradeEvents)(addressEvents);
4875
+ await (0, import_timescale4.upsertHyperliquidWhaleWalletCoverage)({
4876
+ address,
4877
+ fromMs,
4878
+ toMs,
4879
+ coveredFromMs: result2.coveredFromMs,
4880
+ coveredToMs: toMs,
4881
+ status: result2.truncated ? "truncated" : "complete",
4882
+ fillsCount: result2.fills.length,
4883
+ ...identity
4884
+ });
4885
+ fills += result2.fills.length;
4886
+ events += addressEvents.length;
4887
+ if (result2.truncated) truncatedWallets += 1;
4888
+ else completeWallets += 1;
4889
+ } catch (error) {
4890
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
4891
+ failures.push(normalizedError);
4892
+ failedWallets += 1;
4893
+ await (0, import_timescale4.upsertHyperliquidWhaleWalletCoverage)({
4894
+ address,
4895
+ fromMs,
4896
+ toMs,
4897
+ coveredFromMs: null,
4898
+ coveredToMs: null,
4899
+ status: "failed",
4900
+ fillsCount: 0,
4901
+ error: normalizedError.message,
4902
+ ...identity
4903
+ });
4904
+ }
4905
+ }
4906
+ walletsProcessed += 1;
4907
+ params.log?.(
4908
+ `Hyperliquid whales ${walletsProcessed}/${whales.addresses.length}: complete=${completeWallets}, truncated=${truncatedWallets}, failed=${failedWallets}, fills=${fills}, events=${events}`
4909
+ );
4910
+ }
4911
+ });
4912
+ const buckets = await (0, import_timescale4.rebuildHyperliquidWhaleFlowRows)({
4913
+ fromMs,
4914
+ toMs,
4915
+ ...identity
4916
+ });
4917
+ const coverageBuckets = await (0, import_timescale4.rebuildHyperliquidWhaleCoverageRows)({
4918
+ fromMs,
4919
+ toMs,
4920
+ expectedWhales: whales.addresses.length,
4921
+ ...identity
4922
+ });
4923
+ const result = {
4924
+ cached: false,
4925
+ cacheOnly: false,
4926
+ fills,
4927
+ events,
4928
+ buckets,
4929
+ coverageBuckets,
4930
+ complete: truncatedWallets === 0 && failedWallets === 0,
4931
+ walletsProcessed,
4932
+ walletsCached,
4933
+ completeWallets,
4934
+ truncatedWallets,
4935
+ failedWallets,
4936
+ fromMs,
4937
+ toMs
4938
+ };
4939
+ if (params.strict !== false && failures.length) {
4940
+ throw new AggregateError(
4941
+ failures,
4942
+ `Hyperliquid whale backfill failed for ${failures.length}/${whales.addresses.length} wallets`
4943
+ );
4944
+ }
4945
+ return result;
4946
+ };
4947
+
4542
4948
  // src/lib/marketContextPrepare.ts
4543
4949
  var shouldPrepareDerivativesContextForRun = (params) => {
4544
4950
  if (params.universe === "tradfi") return false;
@@ -4589,6 +4995,20 @@ var shouldPrepareCoinMarketCapContextForRun = (params) => {
4589
4995
  cacheOnly: params.cacheOnly
4590
4996
  });
4591
4997
  };
4998
+ var shouldPrepareHyperliquidWhaleContextForRun = (params) => {
4999
+ const strategyRequiresContext = params.strategyNames?.includes(
5000
+ "HyperliquidConsensus"
5001
+ );
5002
+ if (params.universe === "tradfi" || params.mode === "signals" || params.cacheOnly || !params.aiEnabled && !params.mlEnabled && !strategyRequiresContext) {
5003
+ return false;
5004
+ }
5005
+ const configured = String(
5006
+ process.env.HYPERLIQUID_WHALE_BACKFILL_ENABLED ?? ""
5007
+ ).trim().toLowerCase();
5008
+ if (["0", "false", "no", "off"].includes(configured)) return false;
5009
+ if (strategyRequiresContext) return true;
5010
+ return ["1", "true", "yes", "on"].includes(configured);
5011
+ };
4592
5012
  var resolveCoinMarketCapBackfillForMode = (mode) => mode === "backtest" ? backfillCoinMarketCapContextForBacktest : mode === "signals" ? backfillCoinMarketCapContextForSignals : backfillCoinMarketCapContextForReplay;
4593
5013
  var buildCoinMarketCapBackfillParams = (params) => ({
4594
5014
  userName: params.userName,
@@ -4625,6 +5045,18 @@ var prepareMarketContextForRun = async (params) => {
4625
5045
  )
4626
5046
  );
4627
5047
  }
5048
+ if (shouldPrepareHyperliquidWhaleContextForRun(params)) {
5049
+ await timeOperation3(
5050
+ "hyperliquid whale context backfill",
5051
+ () => backfillHyperliquidWhaleContext({
5052
+ startMs: params.preloadStartMs ?? params.startMs,
5053
+ endMs: params.endMs,
5054
+ cacheOnly: params.cacheOnly,
5055
+ strict: false,
5056
+ log
5057
+ })
5058
+ );
5059
+ }
4628
5060
  if (shouldPrepareBinanceMarketContextForRun(params)) {
4629
5061
  await timeOperation3("binance market context backfill", () => {
4630
5062
  const backfill = params.mode === "backtest" ? backfillBinanceMarketContextForBacktest : params.mode === "signals" ? backfillBinanceMarketContextForSignals : backfillBinanceMarketContextForReplay;
@@ -6209,6 +6641,9 @@ var buildPreparedTestSuite = async ({
6209
6641
  cacheOnly: Boolean(flags.cacheOnly),
6210
6642
  aiEnabled,
6211
6643
  mlEnabled,
6644
+ strategyNames: Array.from(
6645
+ new Set(preparedSuite.map((test) => test.strategyName))
6646
+ ),
6212
6647
  log: (message) => console.log(import_chalk9.default.gray(message))
6213
6648
  });
6214
6649
  return preparedSuite;
@@ -6727,6 +7162,9 @@ var prepareContinuedBacktestMarketContext = async ({
6727
7162
  cacheOnly: manifest.flags.cacheOnly,
6728
7163
  aiEnabled: manifest.flags.ai,
6729
7164
  mlEnabled: manifest.flags.ml,
7165
+ strategyNames: Array.from(
7166
+ new Set(remainingSuite.map((test) => test.strategyName))
7167
+ ),
6730
7168
  log: (message) => console.log(import_chalk10.default.gray(message))
6731
7169
  });
6732
7170
  };