@tradejs/cli 3.1.29-beta.292 → 3.1.29-beta.297

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.
@@ -30,7 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/scripts/backtest.ts
31
31
  var backtest_exports = {};
32
32
  __export(backtest_exports, {
33
- BACKTEST_PRELOAD_DAYS: () => import_constants6.BACKTEST_PRELOAD_DAYS,
33
+ BACKTEST_PRELOAD_DAYS: () => import_constants7.BACKTEST_PRELOAD_DAYS,
34
34
  backtest: () => backtest,
35
35
  chunkTestSuiteBySymbol: () => chunkTestSuiteBySymbol,
36
36
  interval: () => interval,
@@ -62,8 +62,8 @@ var import_chalk10 = __toESM(require("chalk"));
62
62
  var import_backtest3 = require("@tradejs/core/backtest");
63
63
  var import_grid = require("@tradejs/core/grid");
64
64
  var import_data = require("@tradejs/core/data");
65
- var import_redis5 = require("@tradejs/infra/redis");
66
- var import_constants6 = require("@tradejs/core/constants");
65
+ var import_redis6 = require("@tradejs/infra/redis");
66
+ var import_constants7 = require("@tradejs/core/constants");
67
67
 
68
68
  // src/lib/backtest/jevOptions.ts
69
69
  var import_promises = __toESM(require("fs/promises"));
@@ -491,13 +491,12 @@ var getReferenceSymbols = () => {
491
491
  };
492
492
  var intervalToMarketFeatureInterval = (interval2) => {
493
493
  const normalized = String(interval2).trim().toLowerCase();
494
- if (normalized === "1" || normalized === "1m") return "1m";
494
+ if (normalized === "1" || normalized === "1m") return null;
495
495
  if (normalized === "5" || normalized === "5m") return "5m";
496
496
  if (normalized === "60" || normalized === "1h") return "1h";
497
497
  return "15m";
498
498
  };
499
499
  var marketIntervalToConnectorInterval = (interval2) => {
500
- if (interval2 === "1m") return "1";
501
500
  if (interval2 === "5m") return "5";
502
501
  if (interval2 === "1h") return "60";
503
502
  return "15";
@@ -662,6 +661,7 @@ var backfillBinanceMarketContext = async (params, enabled) => {
662
661
  return skippedBackfillResult();
663
662
  }
664
663
  const interval2 = intervalToMarketFeatureInterval(params.interval);
664
+ if (interval2 === null) return skippedBackfillResult();
665
665
  const intervalMs = MARKET_FEATURE_INTERVAL_MS[interval2];
666
666
  const { breadthStartMs, tradeFlowStartMs, endMs } = resolveBinanceMarketContextBackfillWindow(params);
667
667
  if (endMs <= Math.min(breadthStartMs, tradeFlowStartMs)) {
@@ -2242,7 +2242,7 @@ var coinMarketCapFetch = async (params) => {
2242
2242
  };
2243
2243
  var fetchCoinMarketCapIndexRows = async (params) => {
2244
2244
  const rows = [];
2245
- const maxChunkDays = 9;
2245
+ const maxChunkDays = 4;
2246
2246
  const totalChunks = Math.max(
2247
2247
  1,
2248
2248
  Math.ceil(
@@ -2679,11 +2679,27 @@ var backfillCoinMarketCapContext = async (params) => {
2679
2679
  toMs
2680
2680
  })
2681
2681
  );
2682
- const globalDailyReady = globalDailyCached || globalDailyBackfillCovered;
2683
- const referencesDailyReady = referencesDailyCached || referencesDailyBackfillCovered;
2684
- const exchangeLiquidityReady = exchangeLiquidityCached || !exchangeLiquidityEnabled || exchangeLiquidityBackfillCovered;
2685
- const fearGreedReady = fearGreedCached || !fearGreedEnabled || fearGreedBackfillCovered;
2686
- const indexesReady = indexesCached || indexesBackfillCovered;
2682
+ const ready = (dataset, existing) => params.repairDataset ? params.repairDataset !== dataset : existing;
2683
+ const globalDailyReady = ready(
2684
+ "cmc-global-1d",
2685
+ globalDailyCached || globalDailyBackfillCovered
2686
+ );
2687
+ const referencesDailyReady = ready(
2688
+ "cmc-reference_asset-1d",
2689
+ referencesDailyCached || referencesDailyBackfillCovered
2690
+ );
2691
+ const exchangeLiquidityReady = ready(
2692
+ "cmc-exchange_liquidity-1d",
2693
+ exchangeLiquidityCached || !exchangeLiquidityEnabled || exchangeLiquidityBackfillCovered
2694
+ );
2695
+ const fearGreedReady = ready(
2696
+ "cmc-fear_greed-1d",
2697
+ fearGreedCached || !fearGreedEnabled || fearGreedBackfillCovered
2698
+ );
2699
+ const indexesReady = ready(
2700
+ "cmc-index-1d",
2701
+ indexesCached || indexesBackfillCovered
2702
+ );
2687
2703
  let globalStatus = resolveBackfillStatus({
2688
2704
  cached: globalDailyCached,
2689
2705
  covered: globalDailyBackfillCovered
@@ -4777,7 +4793,9 @@ var executeBacktestWorkerPool = async ({
4777
4793
  chunkTestSuite,
4778
4794
  getProgressSnapshot,
4779
4795
  initialCompletedTests = 0,
4780
- totalTests
4796
+ totalTests,
4797
+ onProgress,
4798
+ onIncomplete
4781
4799
  }) => {
4782
4800
  const chunks = chunkTestSuite(testSuite);
4783
4801
  const fastAiWorkerPgPoolMax = resolveFastAiWorkerPgPoolMax(
@@ -4793,7 +4811,9 @@ var executeBacktestWorkerPool = async ({
4793
4811
  );
4794
4812
  let completedTests = initialCompletedTests;
4795
4813
  let renderedTests = 0;
4814
+ let pendingResults = 0;
4796
4815
  let isFinishing = false;
4816
+ let isInterrupted = false;
4797
4817
  let signalHandlersRemoved = false;
4798
4818
  const workers = /* @__PURE__ */ new Set();
4799
4819
  const stopWorkers = () => {
@@ -4804,6 +4824,7 @@ var executeBacktestWorkerPool = async ({
4804
4824
  }
4805
4825
  };
4806
4826
  const handleInterrupt = (signal, exitCode) => {
4827
+ isInterrupted = true;
4807
4828
  stopWorkers();
4808
4829
  void Promise.resolve(onInterrupt?.(signal)).catch((error) => {
4809
4830
  console.error(
@@ -4824,15 +4845,16 @@ var executeBacktestWorkerPool = async ({
4824
4845
  process.removeListener("SIGTERM", handleSigterm);
4825
4846
  };
4826
4847
  const maybeFinish = async () => {
4827
- if (isFinishing) {
4828
- return;
4829
- }
4830
- if (completedTests !== totalExpectedTests || workers.size > 0) {
4848
+ if (isFinishing || isInterrupted || workers.size > 0 || pendingResults > 0) {
4831
4849
  return;
4832
4850
  }
4833
4851
  isFinishing = true;
4834
4852
  try {
4835
- await onFinish();
4853
+ if (completedTests === totalExpectedTests) {
4854
+ await onFinish();
4855
+ } else {
4856
+ await onIncomplete?.();
4857
+ }
4836
4858
  } finally {
4837
4859
  cleanupSignalHandlers();
4838
4860
  }
@@ -4891,14 +4913,22 @@ var executeBacktestWorkerPool = async ({
4891
4913
  return;
4892
4914
  }
4893
4915
  completedTests++;
4894
- await onMessage(msg);
4895
- if (completedTests % progressStep2 === 0 || completedTests === totalExpectedTests) {
4896
- tickProgress();
4916
+ onProgress?.(completedTests);
4917
+ pendingResults++;
4918
+ try {
4919
+ await onMessage(msg);
4920
+ if (completedTests % progressStep2 === 0 || completedTests === totalExpectedTests) {
4921
+ tickProgress();
4922
+ }
4923
+ } finally {
4924
+ pendingResults--;
4897
4925
  }
4926
+ await maybeFinish();
4898
4927
  });
4899
4928
  tester.on("error", (err) => {
4900
4929
  workers.delete(tester);
4901
4930
  onWorkerError(err?.message ?? String(err));
4931
+ void maybeFinish();
4902
4932
  });
4903
4933
  tester.on("exit", (code) => {
4904
4934
  workers.delete(tester);
@@ -5285,7 +5315,9 @@ var executeTestSuite = async ({
5285
5315
  onFinish,
5286
5316
  initialCompletedTests = 0,
5287
5317
  totalTests,
5288
- backtestRunId
5318
+ backtestRunId,
5319
+ onProgress,
5320
+ onIncomplete
5289
5321
  }) => {
5290
5322
  markTestsStarted();
5291
5323
  const { testerWorkerPath, testerNeedsTsRuntime } = resolveTesterWorker();
@@ -5309,6 +5341,8 @@ var executeTestSuite = async ({
5309
5341
  chunkTestSuite: (suite) => chunkTestSuiteBySymbol(suite, effectiveParallel),
5310
5342
  initialCompletedTests,
5311
5343
  totalTests,
5344
+ onProgress,
5345
+ onIncomplete,
5312
5346
  onInterrupt,
5313
5347
  onMessage: async (msg) => {
5314
5348
  if (msg.error) {
@@ -5362,6 +5396,106 @@ var printRunOutro = () => {
5362
5396
  console.log("");
5363
5397
  };
5364
5398
 
5399
+ // src/lib/backtest/observeRun.ts
5400
+ var import_constants6 = require("@tradejs/core/constants");
5401
+ var import_redis5 = require("@tradejs/infra/redis");
5402
+ var import_logger = require("@tradejs/infra/logger");
5403
+ var HEARTBEAT_MS = 2e3;
5404
+ var ACTIVE_TTL_SECONDS = 30;
5405
+ var LINKED_RUN_TTL_SECONDS = 30 * 24 * import_constants6.TTL_1H;
5406
+ var RESEARCH_STUDY = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\/\d{4}-\d{2}-\d{2}-[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
5407
+ var MAX_VALUES_PER_PARAMETER = 12;
5408
+ var MAX_VALUE_LENGTH = 500;
5409
+ var summarizeBacktestParameters = (tests) => {
5410
+ const parameters = /* @__PURE__ */ new Map();
5411
+ for (const test of tests) {
5412
+ for (const [name, value] of Object.entries(test.strategyConfig)) {
5413
+ const serialized = JSON.stringify(value);
5414
+ if (serialized === void 0) continue;
5415
+ const parameter = parameters.get(name) ?? {
5416
+ values: /* @__PURE__ */ new Set(),
5417
+ truncated: false
5418
+ };
5419
+ parameters.set(name, parameter);
5420
+ if (parameter.values.has(serialized)) continue;
5421
+ if (parameter.values.size === MAX_VALUES_PER_PARAMETER || serialized.length > MAX_VALUE_LENGTH) {
5422
+ parameter.truncated = true;
5423
+ } else {
5424
+ parameter.values.add(serialized);
5425
+ }
5426
+ }
5427
+ }
5428
+ return [...parameters].map(([name, { values, truncated }]) => ({
5429
+ name,
5430
+ values: [...values],
5431
+ truncated
5432
+ }));
5433
+ };
5434
+ var observeBacktestRun = async (manifest, initialCompleted, parallel) => {
5435
+ if (process.env.TRADEJS_BACKTEST_WEB_JOB === "1") return;
5436
+ const researchStudy = process.env.TRADEJS_RESEARCH_STUDY?.trim();
5437
+ const linkedStudy = researchStudy && RESEARCH_STUDY.test(researchStudy) ? researchStudy : void 0;
5438
+ if (researchStudy && !linkedStudy) {
5439
+ import_logger.logger.warn(
5440
+ "invalid TRADEJS_RESEARCH_STUDY; expected Strategy/YYYY-MM-DD-slug"
5441
+ );
5442
+ }
5443
+ const { testSuite } = manifest;
5444
+ const firstTest = testSuite[0];
5445
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5446
+ const run = {
5447
+ id: manifest.runId,
5448
+ ...linkedStudy && { researchStudy: linkedStudy },
5449
+ userName: manifest.userName,
5450
+ strategyName: firstTest.strategyName,
5451
+ configId: manifest.config,
5452
+ connector: manifest.connectorName,
5453
+ interval: manifest.interval,
5454
+ startTime: manifest.window.start,
5455
+ endTime: manifest.window.end,
5456
+ symbols: [...new Set(testSuite.map((test) => test.symbol))],
5457
+ settings: {
5458
+ ...manifest.flags,
5459
+ parallel,
5460
+ ...firstTest.executionCosts && {
5461
+ executionCosts: JSON.stringify(firstTest.executionCosts)
5462
+ }
5463
+ },
5464
+ parameters: summarizeBacktestParameters(testSuite),
5465
+ status: "running",
5466
+ completed: initialCompleted,
5467
+ total: testSuite.length,
5468
+ startedAt,
5469
+ updatedAt: startedAt
5470
+ };
5471
+ const save = async (expire) => {
5472
+ run.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
5473
+ try {
5474
+ await import_redis5.cliBacktestRunStorage.put(run, expire);
5475
+ } catch (error) {
5476
+ import_logger.logger.warn("failed to publish CLI backtest progress: %s", String(error));
5477
+ }
5478
+ };
5479
+ await save(ACTIVE_TTL_SECONDS);
5480
+ let pending;
5481
+ const timer = setInterval(() => {
5482
+ pending ??= save(ACTIVE_TTL_SECONDS).finally(() => {
5483
+ pending = void 0;
5484
+ });
5485
+ }, HEARTBEAT_MS).unref();
5486
+ return {
5487
+ progress: (completed) => {
5488
+ run.completed = completed;
5489
+ },
5490
+ finish: async (status) => {
5491
+ clearInterval(timer);
5492
+ await pending;
5493
+ run.status = status;
5494
+ await save(linkedStudy ? LINKED_RUN_TTL_SECONDS : import_constants6.TTL_1H);
5495
+ }
5496
+ };
5497
+ };
5498
+
5365
5499
  // src/scripts/backtest.ts
5366
5500
  if (process.argv.some(
5367
5501
  (arg) => arg === "--live" || String(arg).startsWith("--live=")
@@ -5497,8 +5631,8 @@ var loadBacktestConfig = async () => {
5497
5631
  if (!strategyName) {
5498
5632
  throw new Error("Strategy name not found");
5499
5633
  }
5500
- const backtestConfig = await (0, import_redis5.getData)(
5501
- import_redis5.redisKeys.backtestConfig(userName, flags.config),
5634
+ const backtestConfig = await (0, import_redis6.getData)(
5635
+ import_redis6.redisKeys.backtestConfig(userName, flags.config),
5502
5636
  null
5503
5637
  );
5504
5638
  if (!backtestConfig) {
@@ -5645,8 +5779,8 @@ var finishBacktest = async (testSuite) => {
5645
5779
  console.log(import_chalk10.default.gray(`full report: ${markdownReportPath}`));
5646
5780
  console.log("");
5647
5781
  if (!isFastMode || flags.persistResults) {
5648
- await (0, import_redis5.setData)(
5649
- import_redis5.redisKeys.backtestResults(userName, flags.config, timestamp),
5782
+ await (0, import_redis6.setData)(
5783
+ import_redis6.redisKeys.backtestResults(userName, flags.config, timestamp),
5650
5784
  {
5651
5785
  config: flags.config,
5652
5786
  mode: "config",
@@ -5666,11 +5800,10 @@ var finishBacktest = async (testSuite) => {
5666
5800
  errorTests: errorTests2
5667
5801
  },
5668
5802
  {
5669
- expire: import_constants6.TTL_1M
5803
+ expire: import_constants7.TTL_1M
5670
5804
  }
5671
5805
  );
5672
5806
  }
5673
- process.exit();
5674
5807
  };
5675
5808
  var restoreCompletedBacktestResults = (completed) => {
5676
5809
  for (const { result } of completed) {
@@ -5751,6 +5884,56 @@ var prepareContinuedBacktestMarketContext = async ({
5751
5884
  log: (message) => console.log(import_chalk10.default.gray(message))
5752
5885
  });
5753
5886
  };
5887
+ var runBacktestSuite = async (manifest, testSuite, initialCompletedTests = 0) => {
5888
+ const observation = await observeBacktestRun(
5889
+ manifest,
5890
+ initialCompletedTests,
5891
+ effectiveParallel
5892
+ );
5893
+ try {
5894
+ await executeTestSuite({
5895
+ testSuite,
5896
+ window: manifest.window,
5897
+ preloadStart: manifest.preloadStart,
5898
+ backtestRunId: manifest.runId,
5899
+ initialCompletedTests,
5900
+ totalTests: manifest.testSuite.length,
5901
+ onProgress: observation?.progress,
5902
+ onIncomplete: async () => {
5903
+ await observation?.finish("failed");
5904
+ await markBacktestRunStatus({ run: manifest, status: "interrupted" });
5905
+ process.exit(1);
5906
+ },
5907
+ onResult: async (result) => {
5908
+ await saveBacktestCheckpointResult({
5909
+ result,
5910
+ runId: manifest.runId,
5911
+ userName
5912
+ });
5913
+ trackTopResult(result);
5914
+ updateBestTickerResult(result);
5915
+ },
5916
+ onInterrupt: async () => {
5917
+ await observation?.finish("cancelled");
5918
+ await markBacktestRunStatus({ run: manifest, status: "interrupted" });
5919
+ },
5920
+ onFinish: async () => {
5921
+ try {
5922
+ await markBacktestRunStatus({ run: manifest, status: "completed" });
5923
+ await finishBacktest(manifest.testSuite);
5924
+ await observation?.finish("completed");
5925
+ process.exit();
5926
+ } catch (error) {
5927
+ await observation?.finish("failed");
5928
+ throw error;
5929
+ }
5930
+ }
5931
+ });
5932
+ } catch (error) {
5933
+ await observation?.finish("failed");
5934
+ throw error;
5935
+ }
5936
+ };
5754
5937
  var backtest = async () => {
5755
5938
  resetRunState();
5756
5939
  const config = await loadBacktestConfig();
@@ -5765,33 +5948,10 @@ var backtest = async () => {
5765
5948
  if (!remainingSuite.length) {
5766
5949
  await markBacktestRunStatus({ run: manifest2, status: "completed" });
5767
5950
  await finishBacktest(manifest2.testSuite);
5768
- return;
5951
+ process.exit();
5769
5952
  }
5770
5953
  await prepareContinuedBacktestMarketContext({ manifest: manifest2, remainingSuite });
5771
- await executeTestSuite({
5772
- testSuite: remainingSuite,
5773
- window: manifest2.window,
5774
- preloadStart: manifest2.preloadStart,
5775
- backtestRunId: manifest2.runId,
5776
- initialCompletedTests: completed.length,
5777
- totalTests: manifest2.testSuite.length,
5778
- onResult: async (result) => {
5779
- await saveBacktestCheckpointResult({
5780
- result,
5781
- runId: manifest2.runId,
5782
- userName
5783
- });
5784
- trackTopResult(result);
5785
- updateBestTickerResult(result);
5786
- },
5787
- onInterrupt: async () => {
5788
- await markBacktestRunStatus({ run: manifest2, status: "interrupted" });
5789
- },
5790
- onFinish: async () => {
5791
- await markBacktestRunStatus({ run: manifest2, status: "completed" });
5792
- await finishBacktest(manifest2.testSuite);
5793
- }
5794
- });
5954
+ await runBacktestSuite(manifest2, remainingSuite, completed.length);
5795
5955
  return;
5796
5956
  }
5797
5957
  const preparedRun = await prepareRunEnvironment2(config.strategyName);
@@ -5852,28 +6012,7 @@ var backtest = async () => {
5852
6012
  testSuite
5853
6013
  });
5854
6014
  console.log(import_chalk10.default.gray(`backtest run id: ${manifest.runId}`));
5855
- await executeTestSuite({
5856
- testSuite,
5857
- window: preparedRun.window,
5858
- preloadStart: preparedRun.preloadStart,
5859
- backtestRunId: manifest.runId,
5860
- onResult: async (result) => {
5861
- await saveBacktestCheckpointResult({
5862
- result,
5863
- runId: manifest.runId,
5864
- userName
5865
- });
5866
- trackTopResult(result);
5867
- updateBestTickerResult(result);
5868
- },
5869
- onInterrupt: async () => {
5870
- await markBacktestRunStatus({ run: manifest, status: "interrupted" });
5871
- },
5872
- onFinish: async () => {
5873
- await markBacktestRunStatus({ run: manifest, status: "completed" });
5874
- await finishBacktest(testSuite);
5875
- }
5876
- });
6015
+ await runBacktestSuite(manifest, testSuite);
5877
6016
  };
5878
6017
  var main = backtest;
5879
6018
  // Annotate the CommonJS export names for ESM import in node:
@@ -352,7 +352,7 @@ var buildMarketBreadthRows = ({
352
352
 
353
353
  // src/scripts/binanceMarketIngest.ts
354
354
  import_args.default.example(
355
- "yarn cli:node8g binance:market-ingest --all --symbols BTCUSDT,ETHUSDT --days 0.05 --interval 1m --write",
355
+ "yarn cli:node8g binance:market-ingest --all --symbols BTCUSDT,ETHUSDT --days 0.05 --interval 15m --write",
356
356
  "Ingest Binance public market breadth and historical aggTrades buckets"
357
357
  );
358
358
  import_args.default.option(
@@ -360,7 +360,7 @@ import_args.default.option(
360
360
  "Comma-separated target symbols",
361
361
  "BTCUSDT,ETHUSDT"
362
362
  );
363
- import_args.default.option(["i", "interval"], "Aggregation interval: 1m,5m,15m,1h", "1m");
363
+ import_args.default.option(["i", "interval"], "Aggregation interval: 5m,15m,1h", "15m");
364
364
  import_args.default.option(["d", "days"], "Lookback window in days", "1");
365
365
  import_args.default.option(["h", "hours"], "Lookback window in hours; overrides --days");
366
366
  import_args.default.option(["a", "aggTrades"], "Fetch and bucket Binance aggTrades");
@@ -394,7 +394,6 @@ var asInt = (value, fallback) => {
394
394
  };
395
395
  var sleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
396
396
  var intervalToConnectorInterval = (interval) => {
397
- if (interval === "1m") return "1";
398
397
  if (interval === "5m") return "5";
399
398
  if (interval === "15m") return "15";
400
399
  return "60";
@@ -450,6 +449,7 @@ var printEstimate = (estimate) => {
450
449
  var main = async () => {
451
450
  const symbols = normalizeBinanceSymbols(flags.symbols);
452
451
  const interval = normalizeMarketFeatureInterval(flags.interval);
452
+ if (interval === "1m") throw new Error("Use interval 5m, 15m, or 1h");
453
453
  const hours = flags.hours == null ? null : asFloat(flags.hours, 0);
454
454
  const days = hours != null && hours > 0 ? hours / 24 : asFloat(flags.days, 1);
455
455
  const breadthUniverses = (0, import_strategies.getBinanceBreadthUniverses)();