@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.
- package/dist/cli.js +10049 -4860
- package/dist/lib/aiTrainMetrics.js +23 -0
- package/dist/lib/candlesProviderMigration.js +266 -0
- package/dist/lib/cliArgs.js +40 -0
- package/dist/lib/derivativesContextBackfill.js +487 -0
- package/dist/lib/researchAgent.js +195 -0
- package/dist/lib/runtimeParity.js +234 -0
- package/dist/lib/runtimeSignalsLoader.js +153 -0
- package/dist/lib/runtimeSignalsStorage.js +162 -0
- package/dist/lib/telegramReports.js +91 -0
- package/dist/lib/timeWindow.js +101 -0
- package/dist/scripts/agentRun.js +909 -0
- package/dist/scripts/aiTrain.js +113 -31
- package/dist/scripts/backtest.js +794 -93
- package/dist/scripts/candlesMigrateProvider.js +5302 -0
- package/dist/scripts/continuity.js +1 -1
- package/dist/scripts/migration.js +1 -1
- package/dist/scripts/researchAuto.js +847 -0
- package/dist/scripts/results.js +46 -22
- package/dist/scripts/runtimeParity.js +1688 -0
- package/dist/scripts/signals.js +774 -63
- package/dist/scripts/signalsSummary.js +712 -0
- package/dist/scripts/user-add.js +0 -9
- package/package.json +16 -12
package/dist/scripts/backtest.js
CHANGED
|
@@ -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
|
|
505
|
+
var flagForceColor;
|
|
501
506
|
if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
|
|
502
|
-
|
|
507
|
+
flagForceColor = 0;
|
|
503
508
|
} else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
|
|
504
|
-
|
|
509
|
+
flagForceColor = 1;
|
|
505
510
|
}
|
|
506
|
-
|
|
507
|
-
if (
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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 (
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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,
|
|
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:
|
|
589
|
-
stderr:
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
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(["
|
|
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(["
|
|
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
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 (
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
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(
|
|
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 (
|
|
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 (
|
|
1976
|
-
|
|
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
|
-
|
|
2652
|
+
import_chalk2.default.yellow(
|
|
1999
2653
|
`No tests selected (skip=${testsSkip}, limit=${testsLimit}).`
|
|
2000
2654
|
)
|
|
2001
2655
|
);
|
|
2002
2656
|
return;
|
|
2003
2657
|
}
|
|
2004
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
2732
|
+
`--max-old-space-size=${workerHeapMb}`,
|
|
2049
2733
|
"-r",
|
|
2050
2734
|
"ts-node/register",
|
|
2051
2735
|
"-r",
|
|
2052
2736
|
"tsconfig-paths/register"
|
|
2053
|
-
] : [
|
|
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
|
-
|
|
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:
|
|
2101
|
-
amount: profit > 0 ?
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
|
|
2142
|
-
|
|
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
|
-
|
|
2176
|
-
|
|
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(
|
|
2202
|
-
console.log(
|
|
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(
|
|
2208
|
-
console.log(
|
|
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(`${
|
|
2211
|
-
console.log(`${
|
|
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
|
-
|
|
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
|
+
});
|