@tradejs/cli 1.0.6 → 1.0.9

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.
@@ -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,19 +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);
1640
- import_args.default.option("skip", "Skip first N tests", 0);
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);
1641
2194
  import_args.default.option(["p", "parallel"], "Parallel tasks", MAX_PARALLEL);
1642
2195
  import_args.default.option(["f", "timeframe"], "Timeframe", 15);
1643
- 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);
1644
2200
  import_args.default.option(["u", "updateOnly"], "Only update tickers history", false);
1645
2201
  import_args.default.option(["C", "cacheOnly"], "Do not update tickers history", false);
1646
2202
  import_args.default.option(["c", "config"], "Backtest config", "breakout");
1647
2203
  import_args.default.option(["L", "showTickersList"], "Just show only ticker list", false);
1648
- import_args.default.option(["S", "progressStep"], "Progress step", 100);
2204
+ import_args.default.option(["g", "progressStep"], "Progress step", 100);
1649
2205
  import_args.default.option(["U", "user"], "Use user config", "root");
1650
2206
  import_args.default.option(
1651
- "connector",
2207
+ ["o", "connector"],
1652
2208
  "Connector provider or name for backtest (e.g. bybit, binance, coinbase, custom)",
1653
2209
  "bybit"
1654
2210
  );
@@ -1662,22 +2218,26 @@ import_args.default.option(
1662
2218
  "Write AI prompt rows to per-worker JSONL chunks",
1663
2219
  false
1664
2220
  );
1665
- var normalizedArgv = process.argv.map((arg) => {
1666
- if (arg === "--ML") {
1667
- return "--ml";
1668
- }
1669
- if (arg === "--AI") {
1670
- return "--ai";
1671
- }
1672
- return arg;
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"
1673
2230
  });
1674
- var flags = import_args.default.parse(normalizedArgv);
2231
+ process.argv = normalizedArgv;
2232
+ var flags = import_args.default.parse(process.argv);
1675
2233
  var interval = flags.timeframe.toString();
1676
2234
  var progressStep = Math.max(1, parseInt(String(flags.progressStep), 10));
1677
2235
  var testsLimit = Math.max(0, parseInt(String(flags.tests), 10));
1678
2236
  var testsSkip = Math.max(0, parseInt(String(flags.skip ?? 0), 10));
1679
2237
  var testItemTimeoutMs = 12e4;
1680
- var uuid = (len = 12) => (0, import_uuid.v4)().slice(-len);
2238
+ var workerHeapMb = resolveWorkerHeapMb();
2239
+ var effectiveParallel = resolveEffectiveParallel(flags.parallel);
2240
+ var uuid = (len = 12) => (0, import_node_crypto.randomUUID)().slice(-len);
1681
2241
  var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
1682
2242
  var testerWorkerPathCandidates = [
1683
2243
  // `tradejs` bin runs from dist/cli.js, so bundled commands resolve from `dist`.
@@ -1697,22 +2257,22 @@ if (!testerWorkerPath) {
1697
2257
  }
1698
2258
  var testerNeedsTsRuntime = testerWorkerPath.endsWith(".ts");
1699
2259
  var HEADERS_RESULTS = [
1700
- import_chalk.default.blue("ID"),
1701
- import_chalk.default.yellow("SYMBOL"),
1702
- import_chalk.default.cyan("PROFIT"),
1703
- import_chalk.default.cyan("ORDERS"),
1704
- import_chalk.default.cyan("WIN/LOSS (%)"),
1705
- import_chalk.default.cyan("RISK"),
1706
- 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 (%)")
1707
2267
  ];
1708
2268
  var HEADERS_RESULTS_BY_TICKERS = [
1709
- import_chalk.default.blue("ID"),
1710
- import_chalk.default.yellow("SYMBOL"),
1711
- import_chalk.default.cyan("PROFIT"),
1712
- import_chalk.default.cyan("ORDERS"),
1713
- import_chalk.default.cyan("WIN/LOSS (%)"),
1714
- import_chalk.default.cyan("RISK"),
1715
- 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 (%)")
1716
2276
  ];
1717
2277
  var successTests = 0;
1718
2278
  var errorTests = 0;
@@ -1723,14 +2283,35 @@ var resultArtifactsByTestName = /* @__PURE__ */ new Map();
1723
2283
  var artifactRefCountsByTestName = /* @__PURE__ */ new Map();
1724
2284
  var bestTickerTestNameBySymbol = /* @__PURE__ */ new Map();
1725
2285
  var topResultNames = /* @__PURE__ */ new Set();
2286
+ var persistedTestSummaryByKey = /* @__PURE__ */ new Map();
1726
2287
  var userName = flags.user;
1727
2288
  var runStartedAt = Date.now();
2289
+ var testsStartedAt = runStartedAt;
1728
2290
  var createListIt = () => new ListIt({
1729
2291
  autoAlign: true,
1730
2292
  headerUnderline: true
1731
2293
  });
1732
2294
  var createTable = (headers, rows) => createListIt().setHeaderRow(headers).d(rows).toString();
1733
2295
  var createTimestamp = (date) => (0, import_date_fns.format)(date, "yyyyMMddHHmm");
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
+ };
1734
2315
  var isGoodTest = (result) => result.stat?.orders > 5 && result.stat?.profit > 10;
1735
2316
  var getResultAmount = (result) => result.stat.amount ?? 0;
1736
2317
  var recordError = (error) => {
@@ -1841,13 +2422,27 @@ var isStrategyConfigGrid = (value) => {
1841
2422
  (item) => Array.isArray(item)
1842
2423
  );
1843
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
+ };
1844
2439
  var resolveBacktestConnectorName = async (value) => {
1845
2440
  const connectorName = await (0, import_connectors2.resolveConnectorName)(value, projectRoot);
1846
2441
  if (connectorName) {
1847
2442
  return connectorName;
1848
2443
  }
1849
2444
  console.log(
1850
- import_chalk.default.yellow(
2445
+ import_chalk2.default.yellow(
1851
2446
  `Unknown connector "${String(value || "").trim() || String(value)}". Fallback to ${import_connectors2.DEFAULT_CONNECTOR_NAME}.`
1852
2447
  )
1853
2448
  );
@@ -1870,13 +2465,13 @@ var cacheArtifactsById = async (orderLogId, artifacts) => {
1870
2465
  }
1871
2466
  await Promise.all([
1872
2467
  (0, import_redis.setData)(import_redis.redisKeys.cacheOrders(userName, orderLogId), artifacts.orderLog, {
1873
- expire: import_constants.TTL_1D
2468
+ expire: import_constants2.TTL_1D
1874
2469
  }),
1875
2470
  (0, import_redis.setData)(
1876
2471
  import_redis.redisKeys.cachePositions(userName, orderLogId),
1877
2472
  artifacts.positionLog,
1878
2473
  {
1879
- expire: import_constants.TTL_1D
2474
+ expire: import_constants2.TTL_1D
1880
2475
  }
1881
2476
  )
1882
2477
  ]);
@@ -1893,21 +2488,41 @@ var setTestData = async (test, stat, orderLog) => {
1893
2488
  import_redis.redisKeys.testOrders(test.userName, test.strategyName, test.name),
1894
2489
  orderLog,
1895
2490
  {
1896
- expire: import_constants.TTL_1M
2491
+ expire: import_constants2.TTL_1M
1897
2492
  }
1898
2493
  );
1899
2494
  await (0, import_redis.setData)(
1900
2495
  import_redis.redisKeys.testConfig(test.userName, test.strategyName, test.name),
1901
2496
  test,
1902
2497
  {
1903
- expire: import_constants.TTL_1M
2498
+ expire: import_constants2.TTL_1M
1904
2499
  }
1905
2500
  );
1906
2501
  await (0, import_redis.setData)(
1907
2502
  import_redis.redisKeys.testStat(test.userName, test.strategyName, test.name),
1908
2503
  stat,
1909
2504
  {
1910
- 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
1911
2526
  }
1912
2527
  );
1913
2528
  };
@@ -1943,18 +2558,39 @@ var backtest = async () => {
1943
2558
  const marketConnector = await connectorFactory({
1944
2559
  userName: flags.user
1945
2560
  });
1946
- const tickers = await (0, import_cli.getTickers)(
1947
- marketConnector,
1948
- flags.tickers,
1949
- flags.exclude,
1950
- 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
+ )
1951
2569
  );
1952
2570
  if (flags.showTickersList) {
1953
- 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)));
1954
2572
  return;
1955
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
+ );
1956
2585
  if (!flags.cacheOnly) {
1957
- 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
+ );
1958
2594
  const binanceConnectorCreator = await (0, import_connectors2.getConnectorCreatorByName)(
1959
2595
  import_connectors.ConnectorNames.Binance,
1960
2596
  projectRoot
@@ -1972,8 +2608,22 @@ var backtest = async () => {
1972
2608
  const coinbaseConnector = await coinbaseConnectorCreator({
1973
2609
  userName: flags.user
1974
2610
  });
1975
- await (0, import_cli.update)(binanceConnector, interval, ["BTCUSDT"]);
1976
- 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
+ );
1977
2627
  }
1978
2628
  if (flags.updateOnly) {
1979
2629
  return;
@@ -1989,19 +2639,40 @@ var backtest = async () => {
1989
2639
  const aiEnabled = Boolean(flags.ai);
1990
2640
  testSuite = testSuite.map((test) => ({
1991
2641
  ...test,
2642
+ options: {
2643
+ start: window2.start,
2644
+ end: window2.end
2645
+ },
1992
2646
  ml: mlEnabled,
1993
2647
  ai: aiEnabled,
1994
2648
  timeoutMs: testItemTimeoutMs
1995
2649
  })).slice(testsSkip, testsSkip + testsLimit);
1996
2650
  if (!testSuite.length) {
1997
2651
  console.log(
1998
- import_chalk.default.yellow(
2652
+ import_chalk2.default.yellow(
1999
2653
  `No tests selected (skip=${testsSkip}, limit=${testsLimit}).`
2000
2654
  )
2001
2655
  );
2002
2656
  return;
2003
2657
  }
2004
- const chunkSize = Math.ceil(testSuite.length / parseInt(flags.parallel));
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);
2005
2676
  const chunks = import_lodash.default.chunk(testSuite, chunkSize);
2006
2677
  let completedTests = 0;
2007
2678
  let isFinishing = false;
@@ -2031,9 +2702,22 @@ var backtest = async () => {
2031
2702
  stopWorkers();
2032
2703
  process.exit(143);
2033
2704
  });
2034
- 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
+ );
2035
2719
  console.log("");
2036
- const bar = new import_progress.default(
2720
+ const bar = new import_progress2.default(
2037
2721
  ":current/:total [:bar][:percent] :symbol :amount :eta(s)",
2038
2722
  {
2039
2723
  total: testSuite.length,
@@ -2045,12 +2729,12 @@ var backtest = async () => {
2045
2729
  const chunkWithId = chunk.map((test) => ({ ...test, chunkId }));
2046
2730
  const tester = (0, import_child_process.fork)(testerWorkerPath, [], {
2047
2731
  execArgv: testerNeedsTsRuntime ? [
2048
- "--max-old-space-size=8192",
2732
+ `--max-old-space-size=${workerHeapMb}`,
2049
2733
  "-r",
2050
2734
  "ts-node/register",
2051
2735
  "-r",
2052
2736
  "tsconfig-paths/register"
2053
- ] : ["--max-old-space-size=8192"]
2737
+ ] : [`--max-old-space-size=${workerHeapMb}`]
2054
2738
  });
2055
2739
  workers.add(tester);
2056
2740
  tester.on("message", async (msg) => {
@@ -2068,7 +2752,7 @@ var backtest = async () => {
2068
2752
  payload: msg
2069
2753
  });
2070
2754
  console.error(
2071
- 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)}`)
2072
2756
  );
2073
2757
  return;
2074
2758
  } else {
@@ -2097,26 +2781,26 @@ var backtest = async () => {
2097
2781
  bar.tick(
2098
2782
  completedTests === testSuite.length ? completedTests % progressStep : progressStep,
2099
2783
  {
2100
- symbol: import_chalk.default.yellow(symbol),
2101
- 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)
2102
2786
  }
2103
2787
  );
2104
2788
  }
2105
2789
  });
2106
2790
  tester.on("error", (err) => {
2107
2791
  recordError({ error: err?.message ?? err });
2108
- console.error(import_chalk.default.red(`Worker error: ${err.message}`));
2792
+ console.error(import_chalk2.default.red(`Worker error: ${err.message}`));
2109
2793
  });
2110
2794
  tester.on("exit", (code) => {
2111
2795
  workers.delete(tester);
2112
2796
  if (code !== 0) {
2113
2797
  recordError({ error: `Worker exited with code ${code}` });
2114
- console.error(import_chalk.default.red(`Worker exited with code ${code}`));
2798
+ console.error(import_chalk2.default.red(`Worker exited with code ${code}`));
2115
2799
  }
2116
2800
  void maybeFinish();
2117
2801
  });
2118
2802
  await (0, import_redis.setData)(import_redis.redisKeys.cacheChunk(userName, chunkId), chunkWithId, {
2119
- expire: import_constants.TTL_1D
2803
+ expire: import_constants2.TTL_1D
2120
2804
  });
2121
2805
  tester.send({ chunkId, userName });
2122
2806
  }
@@ -2138,8 +2822,8 @@ var saveAndPrintResults = async () => {
2138
2822
  }
2139
2823
  await setTestData(test, stat, orderLog);
2140
2824
  const statRow = [
2141
- import_chalk.default.blue(name),
2142
- import_chalk.default.yellow(symbol),
2825
+ import_chalk2.default.blue(name),
2826
+ import_chalk2.default.yellow(symbol),
2143
2827
  ...(0, import_cli.drawStatInCLI)(stat, [
2144
2828
  "netProfit",
2145
2829
  "orders",
@@ -2172,8 +2856,8 @@ var saveAndPrintResultsByTickers = async () => {
2172
2856
  }
2173
2857
  await setTestData(test, stat, orderLog);
2174
2858
  const statRow = [
2175
- import_chalk.default.blue(name),
2176
- import_chalk.default.yellow(symbol),
2859
+ import_chalk2.default.blue(name),
2860
+ import_chalk2.default.yellow(symbol),
2177
2861
  ...(0, import_cli.drawStatInCLI)(stat, [
2178
2862
  "netProfit",
2179
2863
  "orders",
@@ -2197,18 +2881,26 @@ var finish = async () => {
2197
2881
  } else {
2198
2882
  await saveAndPrintResultsByTickers();
2199
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("");
2200
2892
  const bestConfig = results[0]?.test.strategyConfig;
2201
- console.log(import_chalk.default.gray("BEST CONFIG:"));
2202
- 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)));
2203
2895
  console.log("");
2204
2896
  const mergedConfig = (0, import_backtest.mergeConfigs)(
2205
2897
  results.map(({ test: { strategyConfig } }) => strategyConfig)
2206
2898
  );
2207
- console.log(import_chalk.default.gray("MERGED CONFIG:"));
2208
- 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)));
2209
2901
  console.log("");
2210
- console.log(`${import_chalk.default.green("SUCCESS TESTS")}: ${successTests}`);
2211
- 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}`);
2212
2904
  console.log("");
2213
2905
  const finishedAt = /* @__PURE__ */ new Date();
2214
2906
  const durationSeconds = Number(
@@ -2237,4 +2929,13 @@ var finish = async () => {
2237
2929
  );
2238
2930
  process.exit();
2239
2931
  };
2240
- 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
+ });