@tradejs/cli 1.0.5 → 1.0.8

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.
Files changed (34) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +13791 -7652
  3. package/dist/lib/aiTrainMetrics.js +173 -0
  4. package/dist/lib/candlesProviderMigration.js +266 -0
  5. package/dist/lib/cliArgs.js +40 -0
  6. package/dist/lib/derivativesContextBackfill.js +487 -0
  7. package/dist/lib/researchAgent.js +195 -0
  8. package/dist/lib/runtimeParity.js +234 -0
  9. package/dist/lib/telegramReports.js +91 -0
  10. package/dist/lib/timeWindow.js +101 -0
  11. package/dist/scripts/agentRun.js +895 -0
  12. package/dist/scripts/aiExport.js +192 -0
  13. package/dist/scripts/aiExportSelect.js +135 -0
  14. package/dist/scripts/aiTrain.js +2321 -0
  15. package/dist/scripts/backtest.js +998 -111
  16. package/dist/scripts/candlesMigrateProvider.js +5302 -0
  17. package/dist/scripts/continuity.js +1 -1
  18. package/dist/scripts/derivativesIngest.js +5 -0
  19. package/dist/scripts/derivativesIngestCoinalyzeAll.js +21 -12
  20. package/dist/scripts/migration.js +1 -1
  21. package/dist/scripts/mlExport.js +112 -13
  22. package/dist/scripts/mlExportSelect.js +43 -9
  23. package/dist/scripts/mlTrainLatestSelect.js +4 -3
  24. package/dist/scripts/researchAuto.js +845 -0
  25. package/dist/scripts/resolveExportStrategy.js +133 -0
  26. package/dist/scripts/results.js +46 -22
  27. package/dist/scripts/runtimeParity.js +1651 -0
  28. package/dist/scripts/selectStrategy.js +4 -3
  29. package/dist/scripts/signals.js +799 -62
  30. package/dist/scripts/signalsSummary.js +655 -0
  31. package/dist/scripts/spawnSiblingScript.js +71 -0
  32. package/dist/scripts/user-add.js +0 -9
  33. package/dist/workers/testerWorker.js +13 -2
  34. package/package.json +18 -12
@@ -8,6 +8,10 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __commonJS = (cb, mod) => function __require() {
9
9
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
10
  };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
11
15
  var __copyProps = (to, from, except, desc) => {
12
16
  if (from && typeof from === "object" || typeof from === "function") {
13
17
  for (let key of __getOwnPropNames(from))
@@ -24,6 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
28
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
29
  mod
26
30
  ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
32
 
28
33
  // ../../node_modules/debug/node_modules/ms/index.js
29
34
  var require_ms = __commonJS({
@@ -497,19 +502,21 @@ var require_supports_color = __commonJS({
497
502
  var tty = require("tty");
498
503
  var hasFlag = require_has_flag();
499
504
  var { env } = process;
500
- var forceColor;
505
+ var flagForceColor;
501
506
  if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
502
- forceColor = 0;
507
+ flagForceColor = 0;
503
508
  } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
504
- forceColor = 1;
509
+ flagForceColor = 1;
505
510
  }
506
- if ("FORCE_COLOR" in env) {
507
- if (env.FORCE_COLOR === "true") {
508
- forceColor = 1;
509
- } else if (env.FORCE_COLOR === "false") {
510
- forceColor = 0;
511
- } else {
512
- forceColor = env.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env.FORCE_COLOR, 10), 3);
511
+ function envForceColor() {
512
+ if ("FORCE_COLOR" in env) {
513
+ if (env.FORCE_COLOR === "true") {
514
+ return 1;
515
+ }
516
+ if (env.FORCE_COLOR === "false") {
517
+ return 0;
518
+ }
519
+ return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
513
520
  }
514
521
  }
515
522
  function translateLevel(level) {
@@ -523,15 +530,22 @@ var require_supports_color = __commonJS({
523
530
  has16m: level >= 3
524
531
  };
525
532
  }
526
- function supportsColor(haveStream, streamIsTTY) {
533
+ function supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
534
+ const noFlagForceColor = envForceColor();
535
+ if (noFlagForceColor !== void 0) {
536
+ flagForceColor = noFlagForceColor;
537
+ }
538
+ const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
527
539
  if (forceColor === 0) {
528
540
  return 0;
529
541
  }
530
- if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
531
- return 3;
532
- }
533
- if (hasFlag("color=256")) {
534
- return 2;
542
+ if (sniffFlags) {
543
+ if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
544
+ return 3;
545
+ }
546
+ if (hasFlag("color=256")) {
547
+ return 2;
548
+ }
535
549
  }
536
550
  if (haveStream && !streamIsTTY && forceColor === void 0) {
537
551
  return 0;
@@ -548,7 +562,7 @@ var require_supports_color = __commonJS({
548
562
  return 1;
549
563
  }
550
564
  if ("CI" in env) {
551
- if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "GITHUB_ACTIONS", "BUILDKITE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
565
+ if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "GITHUB_ACTIONS", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
552
566
  return 1;
553
567
  }
554
568
  return min;
@@ -560,7 +574,7 @@ var require_supports_color = __commonJS({
560
574
  return 3;
561
575
  }
562
576
  if ("TERM_PROGRAM" in env) {
563
- const version = parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
577
+ const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
564
578
  switch (env.TERM_PROGRAM) {
565
579
  case "iTerm.app":
566
580
  return version >= 3 ? 3 : 2;
@@ -579,14 +593,17 @@ var require_supports_color = __commonJS({
579
593
  }
580
594
  return min;
581
595
  }
582
- function getSupportLevel(stream) {
583
- const level = supportsColor(stream, stream && stream.isTTY);
596
+ function getSupportLevel(stream, options = {}) {
597
+ const level = supportsColor(stream, {
598
+ streamIsTTY: stream && stream.isTTY,
599
+ ...options
600
+ });
584
601
  return translateLevel(level);
585
602
  }
586
603
  module2.exports = {
587
604
  supportsColor: getSupportLevel,
588
- stdout: translateLevel(supportsColor(true, tty.isatty(1))),
589
- stderr: translateLevel(supportsColor(true, tty.isatty(2)))
605
+ stdout: getSupportLevel({ isTTY: tty.isatty(1) }),
606
+ stderr: getSupportLevel({ isTTY: tty.isatty(2) })
590
607
  };
591
608
  }
592
609
  });
@@ -1610,25 +1627,561 @@ var require_list_it = __commonJS({
1610
1627
  });
1611
1628
 
1612
1629
  // src/scripts/backtest.ts
1630
+ var backtest_exports = {};
1631
+ __export(backtest_exports, {
1632
+ backtest: () => backtest,
1633
+ mergePersistedTestSummaries: () => mergePersistedTestSummaries,
1634
+ resolveEffectiveParallel: () => resolveEffectiveParallel,
1635
+ resolveWorkerHeapMb: () => resolveWorkerHeapMb
1636
+ });
1637
+ module.exports = __toCommonJS(backtest_exports);
1613
1638
  var import_args = __toESM(require("args"));
1614
- var import_progress = __toESM(require("progress"));
1639
+ var import_progress2 = __toESM(require("progress"));
1615
1640
  var import_child_process = require("child_process");
1616
1641
  var import_fs = __toESM(require("fs"));
1617
1642
  var import_path = __toESM(require("path"));
1618
1643
  var import_os = __toESM(require("os"));
1619
- var import_chalk = __toESM(require("chalk"));
1644
+ var import_node_crypto = require("crypto");
1645
+ var import_chalk2 = __toESM(require("chalk"));
1620
1646
  var import_lodash = __toESM(require("lodash"));
1621
1647
  var import_date_fns = require("date-fns");
1622
- var import_uuid = require("uuid");
1623
1648
  var import_connectors = require("@tradejs/connectors");
1624
1649
  var import_connectors2 = require("@tradejs/node/connectors");
1625
1650
  var import_cli = require("@tradejs/node/cli");
1626
1651
  var import_backtest = require("@tradejs/core/backtest");
1627
1652
  var import_data = require("@tradejs/core/data");
1628
- var import_constants = require("@tradejs/core/constants");
1653
+ var import_constants2 = require("@tradejs/core/constants");
1654
+ var import_time = require("@tradejs/core/time");
1629
1655
  var import_redis = require("@tradejs/infra/redis");
1656
+
1657
+ // src/lib/derivativesContextBackfill.ts
1658
+ var import_chalk = __toESM(require("chalk"));
1659
+ var import_progress = __toESM(require("progress"));
1660
+ var import_async = require("@tradejs/core/async");
1661
+ var import_constants = require("@tradejs/core/constants");
1662
+ var import_indicators = require("@tradejs/core/indicators");
1663
+ var import_timescale = require("@tradejs/infra/timescale");
1664
+ var import_userSettings = require("@tradejs/infra/userSettings");
1665
+ var HOUR_MS = 60 * 60 * 1e3;
1666
+ var DAY_MS = 24 * HOUR_MS;
1667
+ var DEFAULT_LOOKBACK_HOURS = 48;
1668
+ var DEFAULT_INTERVALS = ["15m", "1h"];
1669
+ var coinalyzeIntervalMap = {
1670
+ "15m": "15min",
1671
+ "1h": "1hour"
1672
+ };
1673
+ var lastRequestTs = 0;
1674
+ var asInt = (value, fallback) => {
1675
+ const parsed = Number.parseInt(String(value ?? ""), 10);
1676
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
1677
+ };
1678
+ var parseList = (value) => String(value ?? "").split(",").map((item) => item.trim().toUpperCase()).filter(Boolean);
1679
+ var normalizeSymbols = (symbols) => [
1680
+ ...new Set(
1681
+ symbols.map(
1682
+ (symbol) => String(symbol || "").trim().toUpperCase()
1683
+ ).filter(Boolean)
1684
+ )
1685
+ ];
1686
+ var resolveDerivativesContextBackfillSymbols = (_requestedSymbols = []) => [...import_constants.DERIVATIVES_CONTEXT_REFERENCE_SYMBOLS];
1687
+ var chunkArray = (items, size) => {
1688
+ const chunks = [];
1689
+ for (let i = 0; i < items.length; i += size) {
1690
+ chunks.push(items.slice(i, i + size));
1691
+ }
1692
+ return chunks;
1693
+ };
1694
+ var isBacktestDerivativesContextEnabled = () => {
1695
+ return isDerivativesContextBackfillEnabled("BACKTEST");
1696
+ };
1697
+ var isDerivativesContextBackfillEnabled = (env) => {
1698
+ const normalized = String(process.env.DERIVATIVES_CONTEXT_ENABLED ?? "").trim().toLowerCase();
1699
+ if (!normalized) return false;
1700
+ if (["1", "true", "yes", "on"].includes(normalized)) {
1701
+ return true;
1702
+ }
1703
+ if (normalized === "backtest") {
1704
+ return env === "BACKTEST";
1705
+ }
1706
+ if (normalized === "live") {
1707
+ return env !== "BACKTEST";
1708
+ }
1709
+ return false;
1710
+ };
1711
+ var shouldBackfillDerivativesContextForBacktest = (params) => !params.cacheOnly && (params.aiEnabled || params.mlEnabled) && isBacktestDerivativesContextEnabled();
1712
+ var resolveDerivativesContextIntervals = () => {
1713
+ const intervals = (0, import_indicators.normalizeDerivativesIntervals)(
1714
+ process.env.DERIVATIVES_CONTEXT_INTERVALS
1715
+ );
1716
+ return intervals.length ? intervals : DEFAULT_INTERVALS;
1717
+ };
1718
+ var resolveDerivativesContextLookbackMs = () => {
1719
+ const hours = Number(process.env.DERIVATIVES_CONTEXT_LOOKBACK_HOURS);
1720
+ const normalizedHours = Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_LOOKBACK_HOURS;
1721
+ return normalizedHours * HOUR_MS;
1722
+ };
1723
+ var resolveDerivativesContextBackfillWindow = (params) => {
1724
+ const { startMs, endMs, preloadStartMs, nowMs = Date.now() } = params;
1725
+ const safeEndMs = Math.min(endMs, nowMs);
1726
+ const safeStartMs = Math.max(0, Math.min(startMs, safeEndMs));
1727
+ const lookbackFromMs = safeStartMs - resolveDerivativesContextLookbackMs();
1728
+ const requestedFromMs = preloadStartMs != null ? Math.max(Number(preloadStartMs), lookbackFromMs) : lookbackFromMs;
1729
+ const fromMs = Math.max(
1730
+ 0,
1731
+ Math.min(
1732
+ Number.isFinite(requestedFromMs) ? requestedFromMs : safeStartMs,
1733
+ safeStartMs
1734
+ )
1735
+ );
1736
+ return {
1737
+ fromMs: Math.trunc(fromMs),
1738
+ toMs: Math.trunc(safeEndMs),
1739
+ testStartMs: Math.trunc(safeStartMs)
1740
+ };
1741
+ };
1742
+ var getCoinalyzeApiKey = async (userName2) => {
1743
+ const settings = await (0, import_userSettings.getUserSettings)(userName2);
1744
+ return settings.COINALYZE_API_KEY.trim() || String(process.env.COINALYZE_API_KEY ?? "").trim();
1745
+ };
1746
+ var getCoinalyzeBaseUrl = () => process.env.COINALYZE_BASE_URL?.trim() || "https://api.coinalyze.net/v1";
1747
+ var getRequestDelayMs = () => Math.max(
1748
+ 100,
1749
+ asInt(
1750
+ process.env.DERIVATIVES_CONTEXT_BACKFILL_REQUEST_DELAY_MS ?? process.env.COINALYZE_MIN_REQUEST_DELAY_MS,
1751
+ 200
1752
+ )
1753
+ );
1754
+ var getRequestTimeoutMs = () => Math.max(5e3, asInt(process.env.COINALYZE_REQUEST_TIMEOUT_MS, 45e3));
1755
+ var fetchJsonWithRateLimit = async (url, apiKey) => {
1756
+ const requestDelayMs = getRequestDelayMs();
1757
+ const requestTimeoutMs = getRequestTimeoutMs();
1758
+ const maxRetries = asInt(process.env.COINALYZE_MAX_RETRIES, 4);
1759
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
1760
+ const waitMs = Math.max(0, lastRequestTs + requestDelayMs - Date.now());
1761
+ if (waitMs > 0) {
1762
+ await (0, import_async.delay)(waitMs);
1763
+ }
1764
+ lastRequestTs = Date.now();
1765
+ const controller = new AbortController();
1766
+ const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
1767
+ let response;
1768
+ try {
1769
+ response = await fetch(url, {
1770
+ headers: {
1771
+ api_key: apiKey,
1772
+ "x-api-key": apiKey,
1773
+ Authorization: `Bearer ${apiKey}`
1774
+ },
1775
+ signal: controller.signal
1776
+ });
1777
+ } catch (error) {
1778
+ clearTimeout(timer);
1779
+ const abortError = error instanceof Error && (error.name === "AbortError" || String(error.message).toLowerCase().includes("aborted"));
1780
+ if (attempt < maxRetries && abortError) {
1781
+ await (0, import_async.delay)(Math.min(12e3, 800 * 2 ** attempt));
1782
+ continue;
1783
+ }
1784
+ throw error;
1785
+ } finally {
1786
+ clearTimeout(timer);
1787
+ }
1788
+ if (response.ok) {
1789
+ return response.json();
1790
+ }
1791
+ const text = await response.text();
1792
+ const retryAfterRaw = Number(response.headers.get("retry-after") ?? "");
1793
+ const retryAfterMs = Number.isFinite(retryAfterRaw) && retryAfterRaw > 0 ? retryAfterRaw * 1e3 : null;
1794
+ const transient = response.status === 429 || response.status >= 500;
1795
+ if (attempt < maxRetries && transient) {
1796
+ await (0, import_async.delay)(retryAfterMs ?? Math.min(12e3, 800 * 2 ** attempt));
1797
+ continue;
1798
+ }
1799
+ throw new Error(`Coinalyze ${response.status}: ${text}`);
1800
+ }
1801
+ throw new Error("Coinalyze request failed after retries");
1802
+ };
1803
+ var fetchCoinalyzeMarkets = async (apiKey) => {
1804
+ const raw = await fetchJsonWithRateLimit(
1805
+ `${getCoinalyzeBaseUrl()}/future-markets`,
1806
+ apiKey
1807
+ );
1808
+ return Array.isArray(raw) ? raw : [];
1809
+ };
1810
+ var selectBestMarket = (candidates, exchangePriority) => {
1811
+ const scored = candidates.map((item) => {
1812
+ let score = 0;
1813
+ if (item.is_perpetual) score += 50;
1814
+ if (String(item.quote_asset || "").toUpperCase() === "USDT") score += 25;
1815
+ if (item.symbol.includes("_PERP")) score += 10;
1816
+ const exchange = String(item.exchange || "").toUpperCase();
1817
+ const idx = exchangePriority.indexOf(exchange);
1818
+ if (idx >= 0) {
1819
+ score += (exchangePriority.length - idx) * 10;
1820
+ }
1821
+ return { item, score };
1822
+ });
1823
+ scored.sort((a, b) => {
1824
+ if (b.score !== a.score) return b.score - a.score;
1825
+ return a.item.symbol.localeCompare(b.item.symbol);
1826
+ });
1827
+ return scored[0]?.item ?? null;
1828
+ };
1829
+ var buildMatches = (tickers, markets, exchangePriority) => {
1830
+ const marketByTicker = /* @__PURE__ */ new Map();
1831
+ for (const market of markets) {
1832
+ const key = String(market.symbol_on_exchange || "").trim().toUpperCase();
1833
+ if (!key) continue;
1834
+ const list = marketByTicker.get(key) ?? [];
1835
+ list.push(market);
1836
+ marketByTicker.set(key, list);
1837
+ }
1838
+ const matches = [];
1839
+ const unmatched = [];
1840
+ for (const ticker of tickers) {
1841
+ const candidates = marketByTicker.get(ticker) ?? [];
1842
+ const selected = selectBestMarket(candidates, exchangePriority);
1843
+ if (!selected) {
1844
+ unmatched.push(ticker);
1845
+ continue;
1846
+ }
1847
+ matches.push({
1848
+ symbol: ticker,
1849
+ marketSymbol: selected.symbol,
1850
+ exchange: selected.exchange
1851
+ });
1852
+ }
1853
+ return { matches, unmatched };
1854
+ };
1855
+ var normalizeMetricPoint = (metric, point) => {
1856
+ if (metric === "oi") {
1857
+ return {
1858
+ ...point,
1859
+ open_interest: point.open_interest ?? point.openInterest ?? point.oi ?? point.c
1860
+ };
1861
+ }
1862
+ if (metric === "funding") {
1863
+ return {
1864
+ ...point,
1865
+ funding_rate: point.funding_rate ?? point.fundingRate ?? point.rate ?? point.c
1866
+ };
1867
+ }
1868
+ return {
1869
+ ...point,
1870
+ liq_long: point.liq_long ?? point.long_liq ?? point.long ?? point.l,
1871
+ liq_short: point.liq_short ?? point.short_liq ?? point.short ?? point.s
1872
+ };
1873
+ };
1874
+ var toSeriesMap = (raw, metric) => {
1875
+ const out = /* @__PURE__ */ new Map();
1876
+ if (!Array.isArray(raw)) return out;
1877
+ for (const item of raw) {
1878
+ const symbol = String(item.symbol || "").trim().toUpperCase();
1879
+ const history = Array.isArray(item.history) ? item.history : [];
1880
+ if (!symbol) continue;
1881
+ out.set(
1882
+ symbol,
1883
+ history.filter((point) => point && typeof point === "object").map((point) => normalizeMetricPoint(metric, point))
1884
+ );
1885
+ }
1886
+ return out;
1887
+ };
1888
+ var fetchMetricBatch = async (params) => {
1889
+ const { endpoint, metric, marketSymbols, apiKey, interval: interval2, fromMs, toMs } = params;
1890
+ const url = new URL(`${getCoinalyzeBaseUrl()}${endpoint}`);
1891
+ url.searchParams.set("symbols", marketSymbols.join(","));
1892
+ url.searchParams.set("interval", coinalyzeIntervalMap[interval2] || interval2);
1893
+ url.searchParams.set("from", String(Math.floor(fromMs / 1e3)));
1894
+ url.searchParams.set("to", String(Math.floor(toMs / 1e3)));
1895
+ const raw = await fetchJsonWithRateLimit(url.toString(), apiKey);
1896
+ return toSeriesMap(raw, metric);
1897
+ };
1898
+ var skippedBackfillResult = () => ({
1899
+ skipped: true,
1900
+ rows: 0,
1901
+ matchedSymbols: 0,
1902
+ unmatchedSymbols: 0,
1903
+ failedWindows: 0,
1904
+ skippedWindows: 0
1905
+ });
1906
+ var backfillDerivativesContext = async (params, enabled) => {
1907
+ const { userName: userName2, startMs, endMs } = params;
1908
+ const requestedSymbols = normalizeSymbols(params.symbols);
1909
+ const symbols = resolveDerivativesContextBackfillSymbols(requestedSymbols);
1910
+ if (!enabled || !requestedSymbols.length || !symbols.length) {
1911
+ return skippedBackfillResult();
1912
+ }
1913
+ const intervals = resolveDerivativesContextIntervals();
1914
+ if (!intervals.length) {
1915
+ return skippedBackfillResult();
1916
+ }
1917
+ const apiKey = await getCoinalyzeApiKey(userName2);
1918
+ if (!apiKey) {
1919
+ throw new Error(
1920
+ `Missing COINALYZE_API_KEY for derivatives context backfill (user=${userName2})`
1921
+ );
1922
+ }
1923
+ const {
1924
+ fromMs,
1925
+ toMs: safeEndMs,
1926
+ testStartMs: safeStartMs
1927
+ } = resolveDerivativesContextBackfillWindow({
1928
+ startMs,
1929
+ endMs,
1930
+ preloadStartMs: params.preloadStartMs
1931
+ });
1932
+ if (safeEndMs <= fromMs) {
1933
+ return skippedBackfillResult();
1934
+ }
1935
+ const exchangePriority = parseList(
1936
+ process.env.COINALYZE_EXCHANGE_PRIORITY || process.env.DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY || "A,6,0"
1937
+ );
1938
+ const markets = await fetchCoinalyzeMarkets(apiKey);
1939
+ if (!markets.length) {
1940
+ throw new Error("No markets returned by Coinalyze /future-markets");
1941
+ }
1942
+ const { matches, unmatched } = buildMatches(
1943
+ symbols,
1944
+ markets,
1945
+ exchangePriority
1946
+ );
1947
+ if (!matches.length) {
1948
+ throw new Error(
1949
+ "No matched derivatives reference symbols between BTC/ETH and Coinalyze"
1950
+ );
1951
+ }
1952
+ await (0, import_timescale.waitForDbReady)();
1953
+ const batchDays = asInt(
1954
+ process.env.DERIVATIVES_CONTEXT_BACKFILL_BATCH_DAYS,
1955
+ 30
1956
+ );
1957
+ const symbolBatchSize = asInt(
1958
+ process.env.DERIVATIVES_CONTEXT_BACKFILL_SYMBOL_BATCH_SIZE,
1959
+ 8
1960
+ );
1961
+ const symbolBatches = chunkArray(matches, symbolBatchSize);
1962
+ const windowsPerPair = Math.ceil((safeEndMs - fromMs) / (batchDays * DAY_MS));
1963
+ const totalWindows = symbolBatches.length * intervals.length * windowsPerPair;
1964
+ const bar = new import_progress.default(
1965
+ ":current/:total [:bar][:percent] :eta(s) :batch rows=:rows fail=:fail skip=:skip",
1966
+ {
1967
+ total: Math.max(1, totalWindows),
1968
+ width: 24
1969
+ }
1970
+ );
1971
+ const oiPath = process.env.COINALYZE_OI_PATH?.trim() || "/open-interest-history";
1972
+ const fundingPath = process.env.COINALYZE_FUNDING_PATH?.trim() || "/funding-rate-history";
1973
+ const liqPath = process.env.COINALYZE_LIQ_PATH?.trim() || "/liquidation-history";
1974
+ let totalRows = 0;
1975
+ let failedWindows = 0;
1976
+ let skippedWindows = 0;
1977
+ console.log(
1978
+ import_chalk.default.cyan(
1979
+ `derivatives context backfill: referenceSymbols=${matches.length}, requestedSymbols=${requestedSymbols.length}, unmatched=${unmatched.length}, intervals=${intervals.join(",")}, window=${new Date(fromMs).toISOString()}..${new Date(safeEndMs).toISOString()}, testStart=${new Date(safeStartMs).toISOString()}`
1980
+ )
1981
+ );
1982
+ for (const interval2 of intervals) {
1983
+ const edgesBySymbol = await (0, import_timescale.getDerivativesDataEdgesForSymbols)(
1984
+ matches.map((item) => item.symbol),
1985
+ interval2
1986
+ );
1987
+ for (let batchIdx = 0; batchIdx < symbolBatches.length; batchIdx += 1) {
1988
+ const batch = symbolBatches[batchIdx];
1989
+ let cursor = fromMs;
1990
+ while (cursor < safeEndMs) {
1991
+ const toMs = Math.min(safeEndMs, cursor + batchDays * DAY_MS);
1992
+ const missingBatch = batch.filter((item) => {
1993
+ const edges = edgesBySymbol.get(item.symbol.toUpperCase());
1994
+ return edges?.min == null || edges?.max == null || edges.min > cursor || edges.max < toMs;
1995
+ });
1996
+ try {
1997
+ if (!missingBatch.length) {
1998
+ skippedWindows += 1;
1999
+ } else {
2000
+ const marketSymbols = missingBatch.map((item) => item.marketSymbol);
2001
+ const oiMap = await fetchMetricBatch({
2002
+ endpoint: oiPath,
2003
+ metric: "oi",
2004
+ marketSymbols,
2005
+ apiKey,
2006
+ interval: interval2,
2007
+ fromMs: cursor,
2008
+ toMs
2009
+ });
2010
+ const fundingMap = await fetchMetricBatch({
2011
+ endpoint: fundingPath,
2012
+ metric: "funding",
2013
+ marketSymbols,
2014
+ apiKey,
2015
+ interval: interval2,
2016
+ fromMs: cursor,
2017
+ toMs
2018
+ });
2019
+ const liqMap = await fetchMetricBatch({
2020
+ endpoint: liqPath,
2021
+ metric: "liq",
2022
+ marketSymbols,
2023
+ apiKey,
2024
+ interval: interval2,
2025
+ fromMs: cursor,
2026
+ toMs
2027
+ });
2028
+ const rows = missingBatch.flatMap((item) => {
2029
+ const marketSymbol = item.marketSymbol.toUpperCase();
2030
+ const points = (0, import_indicators.mergeCoinalyzeMetrics)({
2031
+ symbol: item.symbol,
2032
+ oiRaw: oiMap.get(marketSymbol) ?? [],
2033
+ fundingRaw: fundingMap.get(marketSymbol) ?? [],
2034
+ liqRaw: liqMap.get(marketSymbol) ?? []
2035
+ });
2036
+ return (0, import_indicators.coinalyzePointsToRows)(points, interval2, "coinalyze");
2037
+ });
2038
+ if (rows.length) {
2039
+ await (0, import_timescale.upsertDerivatives)(rows);
2040
+ totalRows += rows.length;
2041
+ }
2042
+ }
2043
+ } catch (error) {
2044
+ failedWindows += 1;
2045
+ console.error(
2046
+ import_chalk.default.red(
2047
+ `derivatives context backfill window failed batch=${batchIdx + 1}/${symbolBatches.length} interval=${interval2} ${new Date(cursor).toISOString()}..${new Date(toMs).toISOString()}: ${error}`
2048
+ )
2049
+ );
2050
+ } finally {
2051
+ bar.tick(1, {
2052
+ batch: import_chalk.default.gray(
2053
+ `${batchIdx + 1}/${symbolBatches.length} ${interval2}`
2054
+ ),
2055
+ rows: totalRows,
2056
+ fail: failedWindows,
2057
+ skip: skippedWindows
2058
+ });
2059
+ }
2060
+ cursor = toMs + 1;
2061
+ }
2062
+ }
2063
+ }
2064
+ console.log("");
2065
+ console.log(
2066
+ import_chalk.default.green(
2067
+ `derivatives context backfill done: rows=${totalRows}, failed_windows=${failedWindows}, skipped_windows=${skippedWindows}`
2068
+ )
2069
+ );
2070
+ if (failedWindows > 0) {
2071
+ throw new Error(
2072
+ `Derivatives context backfill failed for ${failedWindows} window(s)`
2073
+ );
2074
+ }
2075
+ return {
2076
+ skipped: false,
2077
+ rows: totalRows,
2078
+ matchedSymbols: matches.length,
2079
+ unmatchedSymbols: unmatched.length,
2080
+ failedWindows,
2081
+ skippedWindows
2082
+ };
2083
+ };
2084
+ var backfillDerivativesContextForBacktest = async (params) => backfillDerivativesContext(params, isBacktestDerivativesContextEnabled());
2085
+
2086
+ // src/lib/cliArgs.ts
2087
+ var normalizeCliArgv = (argv, legacyFlagMap) => argv.map((arg) => {
2088
+ const [flag, ...rest] = String(arg).split("=");
2089
+ const normalizedFlag = legacyFlagMap[flag];
2090
+ if (!normalizedFlag) {
2091
+ return arg;
2092
+ }
2093
+ if (!rest.length) {
2094
+ return normalizedFlag;
2095
+ }
2096
+ return `${normalizedFlag}=${rest.join("=")}`;
2097
+ });
2098
+
2099
+ // src/lib/timeWindow.ts
2100
+ var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
2101
+ var toEpochMs = (value) => {
2102
+ if (value == null || value === "") {
2103
+ return null;
2104
+ }
2105
+ const parsed = Number(value);
2106
+ if (!Number.isFinite(parsed) || parsed <= 0) {
2107
+ return null;
2108
+ }
2109
+ return parsed < 1e12 ? Math.trunc(parsed * 1e3) : Math.trunc(parsed);
2110
+ };
2111
+ var toPositiveNumber = (value) => {
2112
+ if (value == null || value === "") {
2113
+ return null;
2114
+ }
2115
+ const parsed = Number(value);
2116
+ if (!Number.isFinite(parsed) || parsed <= 0) {
2117
+ return null;
2118
+ }
2119
+ return parsed;
2120
+ };
2121
+ var resolveTimeWindow = ({
2122
+ days,
2123
+ startTime,
2124
+ endTime,
2125
+ nowMs = Date.now(),
2126
+ defaultStartMs,
2127
+ defaultEndMs
2128
+ }) => {
2129
+ const explicitStart = toEpochMs(startTime);
2130
+ const explicitEnd = toEpochMs(endTime);
2131
+ if (explicitStart != null || explicitEnd != null) {
2132
+ const resolvedEnd2 = explicitEnd ?? defaultEndMs ?? nowMs;
2133
+ const resolvedStart = explicitStart ?? defaultStartMs;
2134
+ if (resolvedStart >= resolvedEnd2) {
2135
+ throw new Error(
2136
+ `Invalid time window: startTime (${resolvedStart}) must be less than endTime (${resolvedEnd2})`
2137
+ );
2138
+ }
2139
+ return {
2140
+ start: resolvedStart,
2141
+ end: resolvedEnd2,
2142
+ source: "explicit"
2143
+ };
2144
+ }
2145
+ const resolvedDays = toPositiveNumber(days);
2146
+ if (resolvedDays != null) {
2147
+ const resolvedEnd2 = defaultEndMs ?? nowMs;
2148
+ const resolvedStart = resolvedEnd2 - resolvedDays * ONE_DAY_MS;
2149
+ if (resolvedStart >= resolvedEnd2) {
2150
+ throw new Error(
2151
+ `Invalid time window: computed start (${resolvedStart}) must be less than end (${resolvedEnd2})`
2152
+ );
2153
+ }
2154
+ return {
2155
+ start: Math.trunc(resolvedStart),
2156
+ end: Math.trunc(resolvedEnd2),
2157
+ source: "days"
2158
+ };
2159
+ }
2160
+ const resolvedEnd = defaultEndMs ?? nowMs;
2161
+ if (defaultStartMs >= resolvedEnd) {
2162
+ throw new Error(
2163
+ `Invalid default time window: start (${defaultStartMs}) must be less than end (${resolvedEnd})`
2164
+ );
2165
+ }
2166
+ return {
2167
+ start: Math.trunc(defaultStartMs),
2168
+ end: Math.trunc(resolvedEnd),
2169
+ source: "default"
2170
+ };
2171
+ };
2172
+
2173
+ // src/scripts/backtest.ts
1630
2174
  var ListIt = require_list_it();
1631
2175
  var MAX_PARALLEL = Math.min(import_os.default.cpus().length, 6);
2176
+ var DEFAULT_WORKER_HEAP_MB = 8192;
2177
+ var resolveWorkerHeapMb = (value = process.env.BACKTEST_WORKER_HEAP_MB, fallback = DEFAULT_WORKER_HEAP_MB) => Math.max(256, parseInt(String(value ?? fallback), 10) || fallback);
2178
+ var resolveEffectiveParallel = (flagValue, envValue = process.env.BACKTEST_MAX_PARALLEL, fallback = MAX_PARALLEL) => Math.max(
2179
+ 1,
2180
+ Math.min(
2181
+ parseInt(String(flagValue), 10) || fallback,
2182
+ parseInt(String(envValue ?? fallback), 10) || fallback
2183
+ )
2184
+ );
1632
2185
  import_args.default.example(
1633
2186
  " yarn backtest -t 400 --cacheOnly",
1634
2187
  "Run tests on uploaded data for 400 tickers"
@@ -1636,18 +2189,22 @@ import_args.default.example(
1636
2189
  import_args.default.option(["t", "tickers"], "Selected tickers");
1637
2190
  import_args.default.option(["e", "exclude"], "Exclude tickers from tests");
1638
2191
  import_args.default.option(["l", "tickersLimit"], "Tickers limit");
1639
- import_args.default.option(["n", "tests"], "Tests limit", import_constants.TESTS_LIMIT);
2192
+ import_args.default.option(["n", "tests"], "Tests limit", import_constants2.TESTS_LIMIT);
2193
+ import_args.default.option(["s", "skip"], "Skip first N tests", 0);
1640
2194
  import_args.default.option(["p", "parallel"], "Parallel tasks", MAX_PARALLEL);
1641
2195
  import_args.default.option(["f", "timeframe"], "Timeframe", 15);
1642
- import_args.default.option(["T", "top"], "Return N best tests", import_constants.TESTS_TOP_LIMIT);
2196
+ import_args.default.option(["d", "days"], "Run backtest only for the last N days");
2197
+ import_args.default.option("startTime", "Explicit backtest start timestamp (ms or seconds)");
2198
+ import_args.default.option("endTime", "Explicit backtest end timestamp (ms or seconds)");
2199
+ import_args.default.option(["T", "top"], "Return N best tests", import_constants2.TESTS_TOP_LIMIT);
1643
2200
  import_args.default.option(["u", "updateOnly"], "Only update tickers history", false);
1644
2201
  import_args.default.option(["C", "cacheOnly"], "Do not update tickers history", false);
1645
2202
  import_args.default.option(["c", "config"], "Backtest config", "breakout");
1646
2203
  import_args.default.option(["L", "showTickersList"], "Just show only ticker list", false);
1647
- import_args.default.option(["S", "progressStep"], "Progress step", 100);
2204
+ import_args.default.option(["g", "progressStep"], "Progress step", 100);
1648
2205
  import_args.default.option(["U", "user"], "Use user config", "root");
1649
2206
  import_args.default.option(
1650
- "connector",
2207
+ ["o", "connector"],
1651
2208
  "Connector provider or name for backtest (e.g. bybit, binance, coinbase, custom)",
1652
2209
  "bybit"
1653
2210
  );
@@ -1656,10 +2213,31 @@ import_args.default.option(
1656
2213
  "Write ML dataset rows to per-worker JSONL chunks",
1657
2214
  false
1658
2215
  );
2216
+ import_args.default.option(
2217
+ ["A", "ai"],
2218
+ "Write AI prompt rows to per-worker JSONL chunks",
2219
+ false
2220
+ );
2221
+ var normalizedArgv = normalizeCliArgv(process.argv, {
2222
+ "--AI": "--ai",
2223
+ "--ML": "--ml",
2224
+ "-C": "--cacheOnly",
2225
+ "-E": "--endTime",
2226
+ "-P": "--progressStep",
2227
+ "-S": "--startTime",
2228
+ "-T": "--top",
2229
+ "-U": "--user"
2230
+ });
2231
+ process.argv = normalizedArgv;
1659
2232
  var flags = import_args.default.parse(process.argv);
1660
2233
  var interval = flags.timeframe.toString();
1661
- var progressStep = flags.progressStep;
1662
- var uuid = (len = 12) => (0, import_uuid.v4)().slice(-len);
2234
+ var progressStep = Math.max(1, parseInt(String(flags.progressStep), 10));
2235
+ var testsLimit = Math.max(0, parseInt(String(flags.tests), 10));
2236
+ var testsSkip = Math.max(0, parseInt(String(flags.skip ?? 0), 10));
2237
+ var testItemTimeoutMs = 12e4;
2238
+ var workerHeapMb = resolveWorkerHeapMb();
2239
+ var effectiveParallel = resolveEffectiveParallel(flags.parallel);
2240
+ var uuid = (len = 12) => (0, import_node_crypto.randomUUID)().slice(-len);
1663
2241
  var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
1664
2242
  var testerWorkerPathCandidates = [
1665
2243
  // `tradejs` bin runs from dist/cli.js, so bundled commands resolve from `dist`.
@@ -1679,40 +2257,163 @@ if (!testerWorkerPath) {
1679
2257
  }
1680
2258
  var testerNeedsTsRuntime = testerWorkerPath.endsWith(".ts");
1681
2259
  var HEADERS_RESULTS = [
1682
- import_chalk.default.blue("ID"),
1683
- import_chalk.default.yellow("SYMBOL"),
1684
- import_chalk.default.cyan("PROFIT"),
1685
- import_chalk.default.cyan("ORDERS"),
1686
- import_chalk.default.cyan("WIN/LOSS (%)"),
1687
- import_chalk.default.cyan("RISK"),
1688
- import_chalk.default.cyan("MAX DRAWDOWN (%)")
2260
+ import_chalk2.default.blue("ID"),
2261
+ import_chalk2.default.yellow("SYMBOL"),
2262
+ import_chalk2.default.cyan("PROFIT"),
2263
+ import_chalk2.default.cyan("ORDERS"),
2264
+ import_chalk2.default.cyan("WIN/LOSS (%)"),
2265
+ import_chalk2.default.cyan("RISK"),
2266
+ import_chalk2.default.cyan("MAX DRAWDOWN (%)")
1689
2267
  ];
1690
2268
  var HEADERS_RESULTS_BY_TICKERS = [
1691
- import_chalk.default.blue("ID"),
1692
- import_chalk.default.yellow("SYMBOL"),
1693
- import_chalk.default.cyan("PROFIT"),
1694
- import_chalk.default.cyan("ORDERS"),
1695
- import_chalk.default.cyan("WIN/LOSS (%)"),
1696
- import_chalk.default.cyan("RISK"),
1697
- import_chalk.default.cyan("MAX DRAWDOWN (%)")
2269
+ import_chalk2.default.blue("ID"),
2270
+ import_chalk2.default.yellow("SYMBOL"),
2271
+ import_chalk2.default.cyan("PROFIT"),
2272
+ import_chalk2.default.cyan("ORDERS"),
2273
+ import_chalk2.default.cyan("WIN/LOSS (%)"),
2274
+ import_chalk2.default.cyan("RISK"),
2275
+ import_chalk2.default.cyan("MAX DRAWDOWN (%)")
1698
2276
  ];
1699
2277
  var successTests = 0;
1700
2278
  var errorTests = 0;
1701
2279
  var errorMessages = [];
1702
2280
  var results = [];
1703
2281
  var resultsByTickers = /* @__PURE__ */ new Map();
2282
+ var resultArtifactsByTestName = /* @__PURE__ */ new Map();
2283
+ var artifactRefCountsByTestName = /* @__PURE__ */ new Map();
2284
+ var bestTickerTestNameBySymbol = /* @__PURE__ */ new Map();
2285
+ var topResultNames = /* @__PURE__ */ new Set();
2286
+ var persistedTestSummaryByKey = /* @__PURE__ */ new Map();
1704
2287
  var userName = flags.user;
1705
2288
  var runStartedAt = Date.now();
2289
+ var testsStartedAt = runStartedAt;
1706
2290
  var createListIt = () => new ListIt({
1707
2291
  autoAlign: true,
1708
2292
  headerUnderline: true
1709
2293
  });
1710
2294
  var createTable = (headers, rows) => createListIt().setHeaderRow(headers).d(rows).toString();
1711
2295
  var createTimestamp = (date) => (0, import_date_fns.format)(date, "yyyyMMddHHmm");
1712
- var filterGoodTests = (tests) => tests.filter((res) => res.stat?.orders > 5 && res.stat?.profit > 10);
2296
+ var formatDuration = (startedAt) => {
2297
+ const seconds = (Date.now() - startedAt) / 1e3;
2298
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
2299
+ const minutes = Math.floor(seconds / 60);
2300
+ const restSeconds = Math.round(seconds % 60);
2301
+ return `${minutes}m ${restSeconds}s`;
2302
+ };
2303
+ var formatWindowDays = (startMs, endMs) => {
2304
+ const days = Math.max(0, (endMs - startMs) / (24 * 60 * 60 * 1e3));
2305
+ return Number.isInteger(days) ? String(days) : days.toFixed(2);
2306
+ };
2307
+ var timeOperation = async (label, operation) => {
2308
+ const startedAt = Date.now();
2309
+ try {
2310
+ return await operation();
2311
+ } finally {
2312
+ console.log(import_chalk2.default.gray(`${label}: done in ${formatDuration(startedAt)}`));
2313
+ }
2314
+ };
2315
+ var isGoodTest = (result) => result.stat?.orders > 5 && result.stat?.profit > 10;
2316
+ var getResultAmount = (result) => result.stat.amount ?? 0;
1713
2317
  var recordError = (error) => {
1714
2318
  errorMessages.push(error);
1715
2319
  };
2320
+ var stripInlineLogs = (result) => {
2321
+ const { inlineOrderLog, inlinePositionLog, ...resultWithoutLogs } = result;
2322
+ return resultWithoutLogs;
2323
+ };
2324
+ var getInlineArtifacts = (result) => {
2325
+ if (!result.inlineOrderLog && !result.inlinePositionLog) {
2326
+ return null;
2327
+ }
2328
+ return {
2329
+ orderLog: result.inlineOrderLog,
2330
+ positionLog: result.inlinePositionLog
2331
+ };
2332
+ };
2333
+ var retainArtifacts = (testName, artifacts) => {
2334
+ if (artifacts) {
2335
+ resultArtifactsByTestName.set(testName, artifacts);
2336
+ }
2337
+ artifactRefCountsByTestName.set(
2338
+ testName,
2339
+ (artifactRefCountsByTestName.get(testName) ?? 0) + 1
2340
+ );
2341
+ };
2342
+ var releaseArtifacts = (testName) => {
2343
+ const nextCount = (artifactRefCountsByTestName.get(testName) ?? 0) - 1;
2344
+ if (nextCount <= 0) {
2345
+ artifactRefCountsByTestName.delete(testName);
2346
+ resultArtifactsByTestName.delete(testName);
2347
+ return;
2348
+ }
2349
+ artifactRefCountsByTestName.set(testName, nextCount);
2350
+ };
2351
+ var insertTopResult = (currentResults, nextResult, limit) => {
2352
+ if (limit <= 0) {
2353
+ return {
2354
+ results: [],
2355
+ added: false
2356
+ };
2357
+ }
2358
+ const nextResults = [...currentResults];
2359
+ const nextAmount = getResultAmount(nextResult);
2360
+ let insertIndex = nextResults.length;
2361
+ for (let index = 0; index < nextResults.length; index += 1) {
2362
+ if (nextAmount > getResultAmount(nextResults[index])) {
2363
+ insertIndex = index;
2364
+ break;
2365
+ }
2366
+ }
2367
+ if (insertIndex === nextResults.length && nextResults.length >= limit) {
2368
+ return {
2369
+ results: currentResults,
2370
+ added: false
2371
+ };
2372
+ }
2373
+ nextResults.splice(insertIndex, 0, nextResult);
2374
+ if (nextResults.length > limit) {
2375
+ nextResults.length = limit;
2376
+ }
2377
+ return {
2378
+ results: nextResults,
2379
+ added: nextResults.some(
2380
+ (candidate) => candidate.test.name === nextResult.test.name
2381
+ )
2382
+ };
2383
+ };
2384
+ var updateTopResults = (nextResults, nextResult, artifacts) => {
2385
+ const previousTopNames = topResultNames;
2386
+ const nextTopNames = new Set(nextResults.map((result) => result.test.name));
2387
+ if (artifacts && !previousTopNames.has(nextResult.test.name) && nextTopNames.has(nextResult.test.name)) {
2388
+ retainArtifacts(nextResult.test.name, artifacts);
2389
+ }
2390
+ for (const testName of previousTopNames) {
2391
+ if (!nextTopNames.has(testName)) {
2392
+ releaseArtifacts(testName);
2393
+ }
2394
+ }
2395
+ results = nextResults;
2396
+ topResultNames = nextTopNames;
2397
+ };
2398
+ var updateBestTickerResult = (result, artifacts) => {
2399
+ if (!isGoodTest(result)) {
2400
+ return false;
2401
+ }
2402
+ const previousResult = resultsByTickers.get(result.test.symbol);
2403
+ if (previousResult && previousResult.stat.profit >= result.stat.profit) {
2404
+ return false;
2405
+ }
2406
+ const previousTestName = bestTickerTestNameBySymbol.get(result.test.symbol);
2407
+ if (previousTestName) {
2408
+ releaseArtifacts(previousTestName);
2409
+ }
2410
+ resultsByTickers.set(result.test.symbol, result);
2411
+ bestTickerTestNameBySymbol.set(result.test.symbol, result.test.name);
2412
+ if (artifacts) {
2413
+ retainArtifacts(result.test.name, artifacts);
2414
+ }
2415
+ return true;
2416
+ };
1716
2417
  var isStrategyConfigGrid = (value) => {
1717
2418
  if (!value || typeof value !== "object" || Array.isArray(value)) {
1718
2419
  return false;
@@ -1721,13 +2422,27 @@ var isStrategyConfigGrid = (value) => {
1721
2422
  (item) => Array.isArray(item)
1722
2423
  );
1723
2424
  };
2425
+ var mergePersistedTestSummaries = (existing, persisted) => {
2426
+ const mergedByKey = /* @__PURE__ */ new Map();
2427
+ for (const item of existing || []) {
2428
+ const strategyName = typeof item?.data?.strategyName === "string" ? item.data.strategyName : "";
2429
+ if (!strategyName || typeof item?.value !== "string") {
2430
+ continue;
2431
+ }
2432
+ mergedByKey.set(`${strategyName}:${item.value}`, item);
2433
+ }
2434
+ for (const [key, item] of persisted) {
2435
+ mergedByKey.set(key, item);
2436
+ }
2437
+ return [...mergedByKey.values()];
2438
+ };
1724
2439
  var resolveBacktestConnectorName = async (value) => {
1725
2440
  const connectorName = await (0, import_connectors2.resolveConnectorName)(value, projectRoot);
1726
2441
  if (connectorName) {
1727
2442
  return connectorName;
1728
2443
  }
1729
2444
  console.log(
1730
- import_chalk.default.yellow(
2445
+ import_chalk2.default.yellow(
1731
2446
  `Unknown connector "${String(value || "").trim() || String(value)}". Fallback to ${import_connectors2.DEFAULT_CONNECTOR_NAME}.`
1732
2447
  )
1733
2448
  );
@@ -1735,33 +2450,79 @@ var resolveBacktestConnectorName = async (value) => {
1735
2450
  };
1736
2451
  var getLogsById = async (orderLogId) => {
1737
2452
  const orderLog = await (0, import_redis.getData)(
1738
- import_redis.redisKeys.cacheOrders(userName, orderLogId)
2453
+ import_redis.redisKeys.cacheOrders(userName, orderLogId),
2454
+ null
1739
2455
  );
1740
2456
  const positionLog = await (0, import_redis.getData)(
1741
- import_redis.redisKeys.cachePositions(userName, orderLogId)
2457
+ import_redis.redisKeys.cachePositions(userName, orderLogId),
2458
+ null
1742
2459
  );
1743
2460
  return { orderLog, positionLog };
1744
2461
  };
2462
+ var cacheArtifactsById = async (orderLogId, artifacts) => {
2463
+ if (!artifacts || !Array.isArray(artifacts.orderLog) || !Array.isArray(artifacts.positionLog)) {
2464
+ return;
2465
+ }
2466
+ await Promise.all([
2467
+ (0, import_redis.setData)(import_redis.redisKeys.cacheOrders(userName, orderLogId), artifacts.orderLog, {
2468
+ expire: import_constants2.TTL_1D
2469
+ }),
2470
+ (0, import_redis.setData)(
2471
+ import_redis.redisKeys.cachePositions(userName, orderLogId),
2472
+ artifacts.positionLog,
2473
+ {
2474
+ expire: import_constants2.TTL_1D
2475
+ }
2476
+ )
2477
+ ]);
2478
+ };
2479
+ var resolveResultArtifacts = async (result) => {
2480
+ const inlineArtifacts = resultArtifactsByTestName.get(result.test.name);
2481
+ if (inlineArtifacts?.orderLog && inlineArtifacts?.positionLog) {
2482
+ return inlineArtifacts;
2483
+ }
2484
+ return getLogsById(result.orderLogId);
2485
+ };
1745
2486
  var setTestData = async (test, stat, orderLog) => {
1746
2487
  await (0, import_redis.setData)(
1747
2488
  import_redis.redisKeys.testOrders(test.userName, test.strategyName, test.name),
1748
2489
  orderLog,
1749
2490
  {
1750
- expire: import_constants.TTL_1M
2491
+ expire: import_constants2.TTL_1M
1751
2492
  }
1752
2493
  );
1753
2494
  await (0, import_redis.setData)(
1754
2495
  import_redis.redisKeys.testConfig(test.userName, test.strategyName, test.name),
1755
2496
  test,
1756
2497
  {
1757
- expire: import_constants.TTL_1M
2498
+ expire: import_constants2.TTL_1M
1758
2499
  }
1759
2500
  );
1760
2501
  await (0, import_redis.setData)(
1761
2502
  import_redis.redisKeys.testStat(test.userName, test.strategyName, test.name),
1762
2503
  stat,
1763
2504
  {
1764
- expire: import_constants.TTL_1M
2505
+ expire: import_constants2.TTL_1M
2506
+ }
2507
+ );
2508
+ const { testId } = (0, import_backtest.parseTestName)(test.name);
2509
+ persistedTestSummaryByKey.set(`${test.strategyName}:${test.name}`, {
2510
+ value: test.name,
2511
+ label: `${test.symbol}_${testId}`,
2512
+ description: `${stat.netProfit || 0}$`,
2513
+ data: {
2514
+ netProfit: stat.netProfit || 0,
2515
+ strategyName: test.strategyName
2516
+ }
2517
+ });
2518
+ };
2519
+ var persistTestSummariesIndex = async () => {
2520
+ const existing = await (0, import_redis.getData)(import_redis.redisKeys.testSummaries(userName), []);
2521
+ await (0, import_redis.setData)(
2522
+ import_redis.redisKeys.testSummaries(userName),
2523
+ mergePersistedTestSummaries(existing, persistedTestSummaryByKey),
2524
+ {
2525
+ expire: 0
1765
2526
  }
1766
2527
  );
1767
2528
  };
@@ -1797,18 +2558,39 @@ var backtest = async () => {
1797
2558
  const marketConnector = await connectorFactory({
1798
2559
  userName: flags.user
1799
2560
  });
1800
- const tickers = await (0, import_cli.getTickers)(
1801
- marketConnector,
1802
- flags.tickers,
1803
- flags.exclude,
1804
- flags.tickersLimit
2561
+ const tickers = await timeOperation(
2562
+ "tickers load",
2563
+ () => (0, import_cli.getTickers)(
2564
+ marketConnector,
2565
+ flags.tickers,
2566
+ flags.exclude,
2567
+ flags.tickersLimit
2568
+ )
1805
2569
  );
1806
2570
  if (flags.showTickersList) {
1807
- console.log(import_chalk.default.gray(JSON.stringify(tickers.sort(), null, 2)));
2571
+ console.log(import_chalk2.default.gray(JSON.stringify(tickers.sort(), null, 2)));
1808
2572
  return;
1809
2573
  }
2574
+ const window2 = resolveTimeWindow({
2575
+ days: flags.days,
2576
+ startTime: flags.startTime,
2577
+ endTime: flags.endTime,
2578
+ defaultStartMs: (0, import_time.getTimestamp)(import_constants2.BACKTEST_DEFAULT_DAYS),
2579
+ defaultEndMs: (0, import_time.getTimestamp)()
2580
+ });
2581
+ const preloadStart = (0, import_time.getBacktestPreloadStart)(
2582
+ window2.start,
2583
+ import_constants2.BACKTEST_PRELOAD_DAYS
2584
+ );
1810
2585
  if (!flags.cacheOnly) {
1811
- await (0, import_cli.update)(marketConnector, interval, tickers);
2586
+ await timeOperation(
2587
+ `update ${connectorName}`,
2588
+ () => (0, import_cli.update)(marketConnector, interval, tickers, void 0, {
2589
+ connectorLabel: connectorName,
2590
+ preloadStart,
2591
+ preloadEnd: window2.end
2592
+ })
2593
+ );
1812
2594
  const binanceConnectorCreator = await (0, import_connectors2.getConnectorCreatorByName)(
1813
2595
  import_connectors.ConnectorNames.Binance,
1814
2596
  projectRoot
@@ -1826,8 +2608,22 @@ var backtest = async () => {
1826
2608
  const coinbaseConnector = await coinbaseConnectorCreator({
1827
2609
  userName: flags.user
1828
2610
  });
1829
- await (0, import_cli.update)(binanceConnector, interval, ["BTCUSDT"]);
1830
- await (0, import_cli.update)(coinbaseConnector, interval, ["BTCUSDT"]);
2611
+ await timeOperation(
2612
+ `update ${import_connectors.ConnectorNames.Binance}`,
2613
+ () => (0, import_cli.update)(binanceConnector, interval, ["BTCUSDT"], void 0, {
2614
+ connectorLabel: import_connectors.ConnectorNames.Binance,
2615
+ preloadStart,
2616
+ preloadEnd: window2.end
2617
+ })
2618
+ );
2619
+ await timeOperation(
2620
+ `update ${import_connectors.ConnectorNames.Coinbase}`,
2621
+ () => (0, import_cli.update)(coinbaseConnector, interval, ["BTCUSDT"], void 0, {
2622
+ connectorLabel: import_connectors.ConnectorNames.Coinbase,
2623
+ preloadStart,
2624
+ preloadEnd: window2.end
2625
+ })
2626
+ );
1831
2627
  }
1832
2628
  if (flags.updateOnly) {
1833
2629
  return;
@@ -1838,15 +2634,59 @@ var backtest = async () => {
1838
2634
  strategyName,
1839
2635
  typedBacktestConfig,
1840
2636
  connectorName
1841
- ).slice(0, parseInt(flags.tests));
2637
+ );
1842
2638
  const mlEnabled = Boolean(flags.ml);
1843
- testSuite = testSuite.map((test) => ({ ...test, ml: mlEnabled }));
1844
- const chunkSize = Math.ceil(testSuite.length / parseInt(flags.parallel));
2639
+ const aiEnabled = Boolean(flags.ai);
2640
+ testSuite = testSuite.map((test) => ({
2641
+ ...test,
2642
+ options: {
2643
+ start: window2.start,
2644
+ end: window2.end
2645
+ },
2646
+ ml: mlEnabled,
2647
+ ai: aiEnabled,
2648
+ timeoutMs: testItemTimeoutMs
2649
+ })).slice(testsSkip, testsSkip + testsLimit);
2650
+ if (!testSuite.length) {
2651
+ console.log(
2652
+ import_chalk2.default.yellow(
2653
+ `No tests selected (skip=${testsSkip}, limit=${testsLimit}).`
2654
+ )
2655
+ );
2656
+ return;
2657
+ }
2658
+ if (shouldBackfillDerivativesContextForBacktest({
2659
+ aiEnabled,
2660
+ cacheOnly: Boolean(flags.cacheOnly),
2661
+ mlEnabled
2662
+ })) {
2663
+ await timeOperation(
2664
+ "derivatives context backfill",
2665
+ () => backfillDerivativesContextForBacktest({
2666
+ userName,
2667
+ symbols: testSuite.map((test) => test.symbol),
2668
+ startMs: window2.start,
2669
+ endMs: window2.end,
2670
+ preloadStartMs: preloadStart
2671
+ })
2672
+ );
2673
+ }
2674
+ testsStartedAt = Date.now();
2675
+ const chunkSize = Math.ceil(testSuite.length / effectiveParallel);
1845
2676
  const chunks = import_lodash.default.chunk(testSuite, chunkSize);
1846
- let completedWorkers = 0;
1847
2677
  let completedTests = 0;
1848
2678
  let isFinishing = false;
1849
2679
  const workers = /* @__PURE__ */ new Set();
2680
+ const maybeFinish = async () => {
2681
+ if (isFinishing) {
2682
+ return;
2683
+ }
2684
+ if (completedTests !== testSuite.length || workers.size > 0) {
2685
+ return;
2686
+ }
2687
+ isFinishing = true;
2688
+ await finish();
2689
+ };
1850
2690
  const stopWorkers = () => {
1851
2691
  for (const worker of workers) {
1852
2692
  if (!worker.killed) {
@@ -1862,9 +2702,22 @@ var backtest = async () => {
1862
2702
  stopWorkers();
1863
2703
  process.exit(143);
1864
2704
  });
1865
- console.log(import_chalk.default.yellow(`tests: ${testSuite.length}`));
2705
+ console.log(import_chalk2.default.yellow(`tests: ${testSuite.length}`));
2706
+ console.log(
2707
+ import_chalk2.default.gray(`parallel: ${effectiveParallel}, workerHeapMb: ${workerHeapMb}`)
2708
+ );
2709
+ console.log(
2710
+ import_chalk2.default.gray(
2711
+ `window: ${(0, import_time.formatUnix)(window2.start)} -> ${(0, import_time.formatUnix)(window2.end)} (${formatWindowDays(window2.start, window2.end)}d, ${window2.source})`
2712
+ )
2713
+ );
2714
+ console.log(
2715
+ import_chalk2.default.gray(
2716
+ `preload: ${(0, import_time.formatUnix)(preloadStart)} -> ${(0, import_time.formatUnix)(window2.end)} (${import_constants2.BACKTEST_PRELOAD_DAYS}d warmup)`
2717
+ )
2718
+ );
1866
2719
  console.log("");
1867
- const bar = new import_progress.default(
2720
+ const bar = new import_progress2.default(
1868
2721
  ":current/:total [:bar][:percent] :symbol :amount :eta(s)",
1869
2722
  {
1870
2723
  total: testSuite.length,
@@ -1876,23 +2729,18 @@ var backtest = async () => {
1876
2729
  const chunkWithId = chunk.map((test) => ({ ...test, chunkId }));
1877
2730
  const tester = (0, import_child_process.fork)(testerWorkerPath, [], {
1878
2731
  execArgv: testerNeedsTsRuntime ? [
1879
- "--max-old-space-size=8192",
2732
+ `--max-old-space-size=${workerHeapMb}`,
1880
2733
  "-r",
1881
2734
  "ts-node/register",
1882
2735
  "-r",
1883
2736
  "tsconfig-paths/register"
1884
- ] : ["--max-old-space-size=8192"]
2737
+ ] : [`--max-old-space-size=${workerHeapMb}`]
1885
2738
  });
1886
2739
  workers.add(tester);
1887
2740
  tester.on("message", async (msg) => {
1888
2741
  if (msg.done) {
1889
- completedWorkers++;
1890
2742
  workers.delete(tester);
1891
- if (completedWorkers === chunks.length && !isFinishing) {
1892
- isFinishing = true;
1893
- results = (0, import_backtest.sortBestTests)(results, flags.top);
1894
- await finish();
1895
- }
2743
+ await maybeFinish();
1896
2744
  return;
1897
2745
  }
1898
2746
  completedTests++;
@@ -1904,49 +2752,55 @@ var backtest = async () => {
1904
2752
  payload: msg
1905
2753
  });
1906
2754
  console.error(
1907
- import_chalk.default.red(`Error in test #${msg.id}: ${JSON.stringify(msg)}`)
2755
+ import_chalk2.default.red(`Error in test #${msg.id}: ${JSON.stringify(msg)}`)
1908
2756
  );
1909
2757
  return;
1910
2758
  } else {
1911
2759
  successTests++;
1912
2760
  }
1913
- results.push(msg);
1914
- const goodResults = filterGoodTests(results);
1915
- goodResults.forEach((res) => {
1916
- const prevValue = resultsByTickers.get(res.test.symbol);
1917
- if (!prevValue || prevValue.stat.profit < res.stat.profit) {
1918
- resultsByTickers.set(res.test.symbol, res);
1919
- }
1920
- });
2761
+ const fullResult = msg;
2762
+ const result = stripInlineLogs(fullResult);
2763
+ const inlineArtifacts = getInlineArtifacts(fullResult);
2764
+ const nextTopResults = insertTopResult(results, result, flags.top);
2765
+ const shouldCacheTopArtifacts = nextTopResults.added;
2766
+ if (nextTopResults.added || results !== nextTopResults.results) {
2767
+ updateTopResults(nextTopResults.results, result, inlineArtifacts);
2768
+ }
2769
+ const updatedBestTickerResult = updateBestTickerResult(
2770
+ result,
2771
+ inlineArtifacts
2772
+ );
2773
+ if ((shouldCacheTopArtifacts || updatedBestTickerResult) && inlineArtifacts) {
2774
+ await cacheArtifactsById(result.orderLogId, inlineArtifacts);
2775
+ }
1921
2776
  if (completedTests % progressStep === 0 || completedTests === testSuite.length) {
1922
- results = (0, import_backtest.sortBestTests)(results, flags.top);
1923
- const {
1924
- test: { symbol, name },
1925
- stat: { profit }
1926
- } = results[0];
2777
+ const bestResult = results[0];
2778
+ const symbol = bestResult?.test.symbol || "-";
2779
+ const profit = bestResult?.stat.profit || 0;
1927
2780
  const profitStr = `${(profit || 0).toFixed(2)}$`;
1928
2781
  bar.tick(
1929
2782
  completedTests === testSuite.length ? completedTests % progressStep : progressStep,
1930
2783
  {
1931
- symbol: import_chalk.default.yellow(symbol),
1932
- amount: profit > 0 ? import_chalk.default.green(profitStr) : import_chalk.default.red(profitStr)
2784
+ symbol: import_chalk2.default.yellow(symbol),
2785
+ amount: profit > 0 ? import_chalk2.default.green(profitStr) : import_chalk2.default.red(profitStr)
1933
2786
  }
1934
2787
  );
1935
2788
  }
1936
2789
  });
1937
2790
  tester.on("error", (err) => {
1938
2791
  recordError({ error: err?.message ?? err });
1939
- console.error(import_chalk.default.red(`Worker error: ${err.message}`));
2792
+ console.error(import_chalk2.default.red(`Worker error: ${err.message}`));
1940
2793
  });
1941
2794
  tester.on("exit", (code) => {
1942
2795
  workers.delete(tester);
1943
2796
  if (code !== 0) {
1944
2797
  recordError({ error: `Worker exited with code ${code}` });
1945
- console.error(import_chalk.default.red(`Worker exited with code ${code}`));
2798
+ console.error(import_chalk2.default.red(`Worker exited with code ${code}`));
1946
2799
  }
2800
+ void maybeFinish();
1947
2801
  });
1948
2802
  await (0, import_redis.setData)(import_redis.redisKeys.cacheChunk(userName, chunkId), chunkWithId, {
1949
- expire: import_constants.TTL_1D
2803
+ expire: import_constants2.TTL_1D
1950
2804
  });
1951
2805
  tester.send({ chunkId, userName });
1952
2806
  }
@@ -1954,14 +2808,22 @@ var backtest = async () => {
1954
2808
  var saveAndPrintResults = async () => {
1955
2809
  const colorizedResults = [];
1956
2810
  for await (const result of results) {
1957
- const { test, orderLogId } = result;
2811
+ const { test } = result;
1958
2812
  const { symbol, name } = test;
1959
- const { orderLog, positionLog } = await getLogsById(orderLogId);
2813
+ const { orderLog, positionLog } = await resolveResultArtifacts(result);
2814
+ if (!orderLog || !positionLog) {
2815
+ throw new Error(`Logs not found for test ${name}`);
2816
+ }
1960
2817
  const stat = (0, import_backtest.calculateStatsFull)(positionLog);
2818
+ if (!stat && result.stat.orders > 0) {
2819
+ throw new Error(
2820
+ `Position log is empty for test ${name} despite ${result.stat.orders} closed orders`
2821
+ );
2822
+ }
1961
2823
  await setTestData(test, stat, orderLog);
1962
2824
  const statRow = [
1963
- import_chalk.default.blue(name),
1964
- import_chalk.default.yellow(symbol),
2825
+ import_chalk2.default.blue(name),
2826
+ import_chalk2.default.yellow(symbol),
1965
2827
  ...(0, import_cli.drawStatInCLI)(stat, [
1966
2828
  "netProfit",
1967
2829
  "orders",
@@ -1980,14 +2842,22 @@ var saveAndPrintResults = async () => {
1980
2842
  var saveAndPrintResultsByTickers = async () => {
1981
2843
  const colorizedResultsByTickers = [];
1982
2844
  for await (const result of resultsByTickers.values()) {
1983
- const { test, orderLogId } = result;
2845
+ const { test } = result;
1984
2846
  const { symbol, name } = test;
1985
- const { orderLog, positionLog } = await getLogsById(orderLogId);
2847
+ const { orderLog, positionLog } = await resolveResultArtifacts(result);
2848
+ if (!orderLog || !positionLog) {
2849
+ throw new Error(`Logs not found for ticker result ${name}`);
2850
+ }
1986
2851
  const stat = (0, import_backtest.calculateStatsFull)(positionLog);
2852
+ if (!stat && result.stat.orders > 0) {
2853
+ throw new Error(
2854
+ `Position log is empty for ticker result ${name} despite ${result.stat.orders} closed orders`
2855
+ );
2856
+ }
1987
2857
  await setTestData(test, stat, orderLog);
1988
2858
  const statRow = [
1989
- import_chalk.default.blue(name),
1990
- import_chalk.default.yellow(symbol),
2859
+ import_chalk2.default.blue(name),
2860
+ import_chalk2.default.yellow(symbol),
1991
2861
  ...(0, import_cli.drawStatInCLI)(stat, [
1992
2862
  "netProfit",
1993
2863
  "orders",
@@ -2011,18 +2881,26 @@ var finish = async () => {
2011
2881
  } else {
2012
2882
  await saveAndPrintResultsByTickers();
2013
2883
  }
2884
+ await persistTestSummariesIndex();
2885
+ console.log(
2886
+ import_chalk2.default.gray(`tests run: done in ${formatDuration(testsStartedAt)}`)
2887
+ );
2888
+ console.log(
2889
+ import_chalk2.default.gray(`backtest total: done in ${formatDuration(runStartedAt)}`)
2890
+ );
2891
+ console.log("");
2014
2892
  const bestConfig = results[0]?.test.strategyConfig;
2015
- console.log(import_chalk.default.gray("BEST CONFIG:"));
2016
- console.log(import_chalk.default.green((0, import_data.toJson)(bestConfig, true)));
2893
+ console.log(import_chalk2.default.gray("BEST CONFIG:"));
2894
+ console.log(import_chalk2.default.green((0, import_data.toJson)(bestConfig, true)));
2017
2895
  console.log("");
2018
2896
  const mergedConfig = (0, import_backtest.mergeConfigs)(
2019
2897
  results.map(({ test: { strategyConfig } }) => strategyConfig)
2020
2898
  );
2021
- console.log(import_chalk.default.gray("MERGED CONFIG:"));
2022
- console.log(import_chalk.default.blue((0, import_data.toJson)(mergedConfig, true)));
2899
+ console.log(import_chalk2.default.gray("MERGED CONFIG:"));
2900
+ console.log(import_chalk2.default.blue((0, import_data.toJson)(mergedConfig, true)));
2023
2901
  console.log("");
2024
- console.log(`${import_chalk.default.green("SUCCESS TESTS")}: ${successTests}`);
2025
- console.log(`${import_chalk.default.red("ERRORS")}: ${errorTests}`);
2902
+ console.log(`${import_chalk2.default.green("SUCCESS TESTS")}: ${successTests}`);
2903
+ console.log(`${import_chalk2.default.red("ERRORS")}: ${errorTests}`);
2026
2904
  console.log("");
2027
2905
  const finishedAt = /* @__PURE__ */ new Date();
2028
2906
  const durationSeconds = Number(
@@ -2038,7 +2916,7 @@ var finish = async () => {
2038
2916
  finishedAt: finishedAt.toISOString(),
2039
2917
  durationSeconds,
2040
2918
  results,
2041
- resultsByTickers: resultsByTickers.values(),
2919
+ resultsByTickers: Array.from(resultsByTickers.values()),
2042
2920
  bestConfig,
2043
2921
  mergedConfig,
2044
2922
  successTests,
@@ -2051,4 +2929,13 @@ var finish = async () => {
2051
2929
  );
2052
2930
  process.exit();
2053
2931
  };
2054
- backtest();
2932
+ if (require.main === module) {
2933
+ void backtest();
2934
+ }
2935
+ // Annotate the CommonJS export names for ESM import in node:
2936
+ 0 && (module.exports = {
2937
+ backtest,
2938
+ mergePersistedTestSummaries,
2939
+ resolveEffectiveParallel,
2940
+ resolveWorkerHeapMb
2941
+ });