@tradejs/cli 3.1.29-beta.291 → 3.1.29-beta.296

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(
@@ -4777,7 +4777,9 @@ var executeBacktestWorkerPool = async ({
4777
4777
  chunkTestSuite,
4778
4778
  getProgressSnapshot,
4779
4779
  initialCompletedTests = 0,
4780
- totalTests
4780
+ totalTests,
4781
+ onProgress,
4782
+ onIncomplete
4781
4783
  }) => {
4782
4784
  const chunks = chunkTestSuite(testSuite);
4783
4785
  const fastAiWorkerPgPoolMax = resolveFastAiWorkerPgPoolMax(
@@ -4793,7 +4795,9 @@ var executeBacktestWorkerPool = async ({
4793
4795
  );
4794
4796
  let completedTests = initialCompletedTests;
4795
4797
  let renderedTests = 0;
4798
+ let pendingResults = 0;
4796
4799
  let isFinishing = false;
4800
+ let isInterrupted = false;
4797
4801
  let signalHandlersRemoved = false;
4798
4802
  const workers = /* @__PURE__ */ new Set();
4799
4803
  const stopWorkers = () => {
@@ -4804,6 +4808,7 @@ var executeBacktestWorkerPool = async ({
4804
4808
  }
4805
4809
  };
4806
4810
  const handleInterrupt = (signal, exitCode) => {
4811
+ isInterrupted = true;
4807
4812
  stopWorkers();
4808
4813
  void Promise.resolve(onInterrupt?.(signal)).catch((error) => {
4809
4814
  console.error(
@@ -4824,15 +4829,16 @@ var executeBacktestWorkerPool = async ({
4824
4829
  process.removeListener("SIGTERM", handleSigterm);
4825
4830
  };
4826
4831
  const maybeFinish = async () => {
4827
- if (isFinishing) {
4828
- return;
4829
- }
4830
- if (completedTests !== totalExpectedTests || workers.size > 0) {
4832
+ if (isFinishing || isInterrupted || workers.size > 0 || pendingResults > 0) {
4831
4833
  return;
4832
4834
  }
4833
4835
  isFinishing = true;
4834
4836
  try {
4835
- await onFinish();
4837
+ if (completedTests === totalExpectedTests) {
4838
+ await onFinish();
4839
+ } else {
4840
+ await onIncomplete?.();
4841
+ }
4836
4842
  } finally {
4837
4843
  cleanupSignalHandlers();
4838
4844
  }
@@ -4891,14 +4897,22 @@ var executeBacktestWorkerPool = async ({
4891
4897
  return;
4892
4898
  }
4893
4899
  completedTests++;
4894
- await onMessage(msg);
4895
- if (completedTests % progressStep2 === 0 || completedTests === totalExpectedTests) {
4896
- tickProgress();
4900
+ onProgress?.(completedTests);
4901
+ pendingResults++;
4902
+ try {
4903
+ await onMessage(msg);
4904
+ if (completedTests % progressStep2 === 0 || completedTests === totalExpectedTests) {
4905
+ tickProgress();
4906
+ }
4907
+ } finally {
4908
+ pendingResults--;
4897
4909
  }
4910
+ await maybeFinish();
4898
4911
  });
4899
4912
  tester.on("error", (err) => {
4900
4913
  workers.delete(tester);
4901
4914
  onWorkerError(err?.message ?? String(err));
4915
+ void maybeFinish();
4902
4916
  });
4903
4917
  tester.on("exit", (code) => {
4904
4918
  workers.delete(tester);
@@ -5285,7 +5299,9 @@ var executeTestSuite = async ({
5285
5299
  onFinish,
5286
5300
  initialCompletedTests = 0,
5287
5301
  totalTests,
5288
- backtestRunId
5302
+ backtestRunId,
5303
+ onProgress,
5304
+ onIncomplete
5289
5305
  }) => {
5290
5306
  markTestsStarted();
5291
5307
  const { testerWorkerPath, testerNeedsTsRuntime } = resolveTesterWorker();
@@ -5309,6 +5325,8 @@ var executeTestSuite = async ({
5309
5325
  chunkTestSuite: (suite) => chunkTestSuiteBySymbol(suite, effectiveParallel),
5310
5326
  initialCompletedTests,
5311
5327
  totalTests,
5328
+ onProgress,
5329
+ onIncomplete,
5312
5330
  onInterrupt,
5313
5331
  onMessage: async (msg) => {
5314
5332
  if (msg.error) {
@@ -5362,6 +5380,106 @@ var printRunOutro = () => {
5362
5380
  console.log("");
5363
5381
  };
5364
5382
 
5383
+ // src/lib/backtest/observeRun.ts
5384
+ var import_constants6 = require("@tradejs/core/constants");
5385
+ var import_redis5 = require("@tradejs/infra/redis");
5386
+ var import_logger = require("@tradejs/infra/logger");
5387
+ var HEARTBEAT_MS = 2e3;
5388
+ var ACTIVE_TTL_SECONDS = 30;
5389
+ var LINKED_RUN_TTL_SECONDS = 30 * 24 * import_constants6.TTL_1H;
5390
+ 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}$/;
5391
+ var MAX_VALUES_PER_PARAMETER = 12;
5392
+ var MAX_VALUE_LENGTH = 500;
5393
+ var summarizeBacktestParameters = (tests) => {
5394
+ const parameters = /* @__PURE__ */ new Map();
5395
+ for (const test of tests) {
5396
+ for (const [name, value] of Object.entries(test.strategyConfig)) {
5397
+ const serialized = JSON.stringify(value);
5398
+ if (serialized === void 0) continue;
5399
+ const parameter = parameters.get(name) ?? {
5400
+ values: /* @__PURE__ */ new Set(),
5401
+ truncated: false
5402
+ };
5403
+ parameters.set(name, parameter);
5404
+ if (parameter.values.has(serialized)) continue;
5405
+ if (parameter.values.size === MAX_VALUES_PER_PARAMETER || serialized.length > MAX_VALUE_LENGTH) {
5406
+ parameter.truncated = true;
5407
+ } else {
5408
+ parameter.values.add(serialized);
5409
+ }
5410
+ }
5411
+ }
5412
+ return [...parameters].map(([name, { values, truncated }]) => ({
5413
+ name,
5414
+ values: [...values],
5415
+ truncated
5416
+ }));
5417
+ };
5418
+ var observeBacktestRun = async (manifest, initialCompleted, parallel) => {
5419
+ if (process.env.TRADEJS_BACKTEST_WEB_JOB === "1") return;
5420
+ const researchStudy = process.env.TRADEJS_RESEARCH_STUDY?.trim();
5421
+ const linkedStudy = researchStudy && RESEARCH_STUDY.test(researchStudy) ? researchStudy : void 0;
5422
+ if (researchStudy && !linkedStudy) {
5423
+ import_logger.logger.warn(
5424
+ "invalid TRADEJS_RESEARCH_STUDY; expected Strategy/YYYY-MM-DD-slug"
5425
+ );
5426
+ }
5427
+ const { testSuite } = manifest;
5428
+ const firstTest = testSuite[0];
5429
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
5430
+ const run = {
5431
+ id: manifest.runId,
5432
+ ...linkedStudy && { researchStudy: linkedStudy },
5433
+ userName: manifest.userName,
5434
+ strategyName: firstTest.strategyName,
5435
+ configId: manifest.config,
5436
+ connector: manifest.connectorName,
5437
+ interval: manifest.interval,
5438
+ startTime: manifest.window.start,
5439
+ endTime: manifest.window.end,
5440
+ symbols: [...new Set(testSuite.map((test) => test.symbol))],
5441
+ settings: {
5442
+ ...manifest.flags,
5443
+ parallel,
5444
+ ...firstTest.executionCosts && {
5445
+ executionCosts: JSON.stringify(firstTest.executionCosts)
5446
+ }
5447
+ },
5448
+ parameters: summarizeBacktestParameters(testSuite),
5449
+ status: "running",
5450
+ completed: initialCompleted,
5451
+ total: testSuite.length,
5452
+ startedAt,
5453
+ updatedAt: startedAt
5454
+ };
5455
+ const save = async (expire) => {
5456
+ run.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
5457
+ try {
5458
+ await import_redis5.cliBacktestRunStorage.put(run, expire);
5459
+ } catch (error) {
5460
+ import_logger.logger.warn("failed to publish CLI backtest progress: %s", String(error));
5461
+ }
5462
+ };
5463
+ await save(ACTIVE_TTL_SECONDS);
5464
+ let pending;
5465
+ const timer = setInterval(() => {
5466
+ pending ??= save(ACTIVE_TTL_SECONDS).finally(() => {
5467
+ pending = void 0;
5468
+ });
5469
+ }, HEARTBEAT_MS).unref();
5470
+ return {
5471
+ progress: (completed) => {
5472
+ run.completed = completed;
5473
+ },
5474
+ finish: async (status) => {
5475
+ clearInterval(timer);
5476
+ await pending;
5477
+ run.status = status;
5478
+ await save(linkedStudy ? LINKED_RUN_TTL_SECONDS : import_constants6.TTL_1H);
5479
+ }
5480
+ };
5481
+ };
5482
+
5365
5483
  // src/scripts/backtest.ts
5366
5484
  if (process.argv.some(
5367
5485
  (arg) => arg === "--live" || String(arg).startsWith("--live=")
@@ -5497,8 +5615,8 @@ var loadBacktestConfig = async () => {
5497
5615
  if (!strategyName) {
5498
5616
  throw new Error("Strategy name not found");
5499
5617
  }
5500
- const backtestConfig = await (0, import_redis5.getData)(
5501
- import_redis5.redisKeys.backtestConfig(userName, flags.config),
5618
+ const backtestConfig = await (0, import_redis6.getData)(
5619
+ import_redis6.redisKeys.backtestConfig(userName, flags.config),
5502
5620
  null
5503
5621
  );
5504
5622
  if (!backtestConfig) {
@@ -5645,8 +5763,8 @@ var finishBacktest = async (testSuite) => {
5645
5763
  console.log(import_chalk10.default.gray(`full report: ${markdownReportPath}`));
5646
5764
  console.log("");
5647
5765
  if (!isFastMode || flags.persistResults) {
5648
- await (0, import_redis5.setData)(
5649
- import_redis5.redisKeys.backtestResults(userName, flags.config, timestamp),
5766
+ await (0, import_redis6.setData)(
5767
+ import_redis6.redisKeys.backtestResults(userName, flags.config, timestamp),
5650
5768
  {
5651
5769
  config: flags.config,
5652
5770
  mode: "config",
@@ -5666,11 +5784,10 @@ var finishBacktest = async (testSuite) => {
5666
5784
  errorTests: errorTests2
5667
5785
  },
5668
5786
  {
5669
- expire: import_constants6.TTL_1M
5787
+ expire: import_constants7.TTL_1M
5670
5788
  }
5671
5789
  );
5672
5790
  }
5673
- process.exit();
5674
5791
  };
5675
5792
  var restoreCompletedBacktestResults = (completed) => {
5676
5793
  for (const { result } of completed) {
@@ -5751,6 +5868,56 @@ var prepareContinuedBacktestMarketContext = async ({
5751
5868
  log: (message) => console.log(import_chalk10.default.gray(message))
5752
5869
  });
5753
5870
  };
5871
+ var runBacktestSuite = async (manifest, testSuite, initialCompletedTests = 0) => {
5872
+ const observation = await observeBacktestRun(
5873
+ manifest,
5874
+ initialCompletedTests,
5875
+ effectiveParallel
5876
+ );
5877
+ try {
5878
+ await executeTestSuite({
5879
+ testSuite,
5880
+ window: manifest.window,
5881
+ preloadStart: manifest.preloadStart,
5882
+ backtestRunId: manifest.runId,
5883
+ initialCompletedTests,
5884
+ totalTests: manifest.testSuite.length,
5885
+ onProgress: observation?.progress,
5886
+ onIncomplete: async () => {
5887
+ await observation?.finish("failed");
5888
+ await markBacktestRunStatus({ run: manifest, status: "interrupted" });
5889
+ process.exit(1);
5890
+ },
5891
+ onResult: async (result) => {
5892
+ await saveBacktestCheckpointResult({
5893
+ result,
5894
+ runId: manifest.runId,
5895
+ userName
5896
+ });
5897
+ trackTopResult(result);
5898
+ updateBestTickerResult(result);
5899
+ },
5900
+ onInterrupt: async () => {
5901
+ await observation?.finish("cancelled");
5902
+ await markBacktestRunStatus({ run: manifest, status: "interrupted" });
5903
+ },
5904
+ onFinish: async () => {
5905
+ try {
5906
+ await markBacktestRunStatus({ run: manifest, status: "completed" });
5907
+ await finishBacktest(manifest.testSuite);
5908
+ await observation?.finish("completed");
5909
+ process.exit();
5910
+ } catch (error) {
5911
+ await observation?.finish("failed");
5912
+ throw error;
5913
+ }
5914
+ }
5915
+ });
5916
+ } catch (error) {
5917
+ await observation?.finish("failed");
5918
+ throw error;
5919
+ }
5920
+ };
5754
5921
  var backtest = async () => {
5755
5922
  resetRunState();
5756
5923
  const config = await loadBacktestConfig();
@@ -5765,33 +5932,10 @@ var backtest = async () => {
5765
5932
  if (!remainingSuite.length) {
5766
5933
  await markBacktestRunStatus({ run: manifest2, status: "completed" });
5767
5934
  await finishBacktest(manifest2.testSuite);
5768
- return;
5935
+ process.exit();
5769
5936
  }
5770
5937
  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
- });
5938
+ await runBacktestSuite(manifest2, remainingSuite, completed.length);
5795
5939
  return;
5796
5940
  }
5797
5941
  const preparedRun = await prepareRunEnvironment2(config.strategyName);
@@ -5852,28 +5996,7 @@ var backtest = async () => {
5852
5996
  testSuite
5853
5997
  });
5854
5998
  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
- });
5999
+ await runBacktestSuite(manifest, testSuite);
5877
6000
  };
5878
6001
  var main = backtest;
5879
6002
  // 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)();