@tradejs/cli 1.0.5 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/cli.js +13791 -7652
- package/dist/lib/aiTrainMetrics.js +173 -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/telegramReports.js +91 -0
- package/dist/lib/timeWindow.js +101 -0
- package/dist/scripts/agentRun.js +895 -0
- package/dist/scripts/aiExport.js +192 -0
- package/dist/scripts/aiExportSelect.js +135 -0
- package/dist/scripts/aiTrain.js +2321 -0
- package/dist/scripts/backtest.js +998 -111
- package/dist/scripts/candlesMigrateProvider.js +5302 -0
- package/dist/scripts/continuity.js +1 -1
- package/dist/scripts/derivativesIngest.js +5 -0
- package/dist/scripts/derivativesIngestCoinalyzeAll.js +21 -12
- package/dist/scripts/migration.js +1 -1
- package/dist/scripts/mlExport.js +112 -13
- package/dist/scripts/mlExportSelect.js +43 -9
- package/dist/scripts/mlTrainLatestSelect.js +4 -3
- package/dist/scripts/researchAuto.js +845 -0
- package/dist/scripts/resolveExportStrategy.js +133 -0
- package/dist/scripts/results.js +46 -22
- package/dist/scripts/runtimeParity.js +1651 -0
- package/dist/scripts/selectStrategy.js +4 -3
- package/dist/scripts/signals.js +799 -62
- package/dist/scripts/signalsSummary.js +655 -0
- package/dist/scripts/spawnSiblingScript.js +71 -0
- package/dist/scripts/user-add.js +0 -9
- package/dist/workers/testerWorker.js +13 -2
- package/package.json +18 -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,18 +2189,22 @@ import_args.default.example(
|
|
|
1636
2189
|
import_args.default.option(["t", "tickers"], "Selected tickers");
|
|
1637
2190
|
import_args.default.option(["e", "exclude"], "Exclude tickers from tests");
|
|
1638
2191
|
import_args.default.option(["l", "tickersLimit"], "Tickers limit");
|
|
1639
|
-
import_args.default.option(["n", "tests"], "Tests limit",
|
|
2192
|
+
import_args.default.option(["n", "tests"], "Tests limit", import_constants2.TESTS_LIMIT);
|
|
2193
|
+
import_args.default.option(["s", "skip"], "Skip first N tests", 0);
|
|
1640
2194
|
import_args.default.option(["p", "parallel"], "Parallel tasks", MAX_PARALLEL);
|
|
1641
2195
|
import_args.default.option(["f", "timeframe"], "Timeframe", 15);
|
|
1642
|
-
import_args.default.option(["
|
|
2196
|
+
import_args.default.option(["d", "days"], "Run backtest only for the last N days");
|
|
2197
|
+
import_args.default.option("startTime", "Explicit backtest start timestamp (ms or seconds)");
|
|
2198
|
+
import_args.default.option("endTime", "Explicit backtest end timestamp (ms or seconds)");
|
|
2199
|
+
import_args.default.option(["T", "top"], "Return N best tests", import_constants2.TESTS_TOP_LIMIT);
|
|
1643
2200
|
import_args.default.option(["u", "updateOnly"], "Only update tickers history", false);
|
|
1644
2201
|
import_args.default.option(["C", "cacheOnly"], "Do not update tickers history", false);
|
|
1645
2202
|
import_args.default.option(["c", "config"], "Backtest config", "breakout");
|
|
1646
2203
|
import_args.default.option(["L", "showTickersList"], "Just show only ticker list", false);
|
|
1647
|
-
import_args.default.option(["
|
|
2204
|
+
import_args.default.option(["g", "progressStep"], "Progress step", 100);
|
|
1648
2205
|
import_args.default.option(["U", "user"], "Use user config", "root");
|
|
1649
2206
|
import_args.default.option(
|
|
1650
|
-
"connector",
|
|
2207
|
+
["o", "connector"],
|
|
1651
2208
|
"Connector provider or name for backtest (e.g. bybit, binance, coinbase, custom)",
|
|
1652
2209
|
"bybit"
|
|
1653
2210
|
);
|
|
@@ -1656,10 +2213,31 @@ import_args.default.option(
|
|
|
1656
2213
|
"Write ML dataset rows to per-worker JSONL chunks",
|
|
1657
2214
|
false
|
|
1658
2215
|
);
|
|
2216
|
+
import_args.default.option(
|
|
2217
|
+
["A", "ai"],
|
|
2218
|
+
"Write AI prompt rows to per-worker JSONL chunks",
|
|
2219
|
+
false
|
|
2220
|
+
);
|
|
2221
|
+
var normalizedArgv = normalizeCliArgv(process.argv, {
|
|
2222
|
+
"--AI": "--ai",
|
|
2223
|
+
"--ML": "--ml",
|
|
2224
|
+
"-C": "--cacheOnly",
|
|
2225
|
+
"-E": "--endTime",
|
|
2226
|
+
"-P": "--progressStep",
|
|
2227
|
+
"-S": "--startTime",
|
|
2228
|
+
"-T": "--top",
|
|
2229
|
+
"-U": "--user"
|
|
2230
|
+
});
|
|
2231
|
+
process.argv = normalizedArgv;
|
|
1659
2232
|
var flags = import_args.default.parse(process.argv);
|
|
1660
2233
|
var interval = flags.timeframe.toString();
|
|
1661
|
-
var progressStep = flags.progressStep;
|
|
1662
|
-
var
|
|
2234
|
+
var progressStep = Math.max(1, parseInt(String(flags.progressStep), 10));
|
|
2235
|
+
var testsLimit = Math.max(0, parseInt(String(flags.tests), 10));
|
|
2236
|
+
var testsSkip = Math.max(0, parseInt(String(flags.skip ?? 0), 10));
|
|
2237
|
+
var testItemTimeoutMs = 12e4;
|
|
2238
|
+
var workerHeapMb = resolveWorkerHeapMb();
|
|
2239
|
+
var effectiveParallel = resolveEffectiveParallel(flags.parallel);
|
|
2240
|
+
var uuid = (len = 12) => (0, import_node_crypto.randomUUID)().slice(-len);
|
|
1663
2241
|
var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
1664
2242
|
var testerWorkerPathCandidates = [
|
|
1665
2243
|
// `tradejs` bin runs from dist/cli.js, so bundled commands resolve from `dist`.
|
|
@@ -1679,40 +2257,163 @@ if (!testerWorkerPath) {
|
|
|
1679
2257
|
}
|
|
1680
2258
|
var testerNeedsTsRuntime = testerWorkerPath.endsWith(".ts");
|
|
1681
2259
|
var HEADERS_RESULTS = [
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
2260
|
+
import_chalk2.default.blue("ID"),
|
|
2261
|
+
import_chalk2.default.yellow("SYMBOL"),
|
|
2262
|
+
import_chalk2.default.cyan("PROFIT"),
|
|
2263
|
+
import_chalk2.default.cyan("ORDERS"),
|
|
2264
|
+
import_chalk2.default.cyan("WIN/LOSS (%)"),
|
|
2265
|
+
import_chalk2.default.cyan("RISK"),
|
|
2266
|
+
import_chalk2.default.cyan("MAX DRAWDOWN (%)")
|
|
1689
2267
|
];
|
|
1690
2268
|
var HEADERS_RESULTS_BY_TICKERS = [
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2269
|
+
import_chalk2.default.blue("ID"),
|
|
2270
|
+
import_chalk2.default.yellow("SYMBOL"),
|
|
2271
|
+
import_chalk2.default.cyan("PROFIT"),
|
|
2272
|
+
import_chalk2.default.cyan("ORDERS"),
|
|
2273
|
+
import_chalk2.default.cyan("WIN/LOSS (%)"),
|
|
2274
|
+
import_chalk2.default.cyan("RISK"),
|
|
2275
|
+
import_chalk2.default.cyan("MAX DRAWDOWN (%)")
|
|
1698
2276
|
];
|
|
1699
2277
|
var successTests = 0;
|
|
1700
2278
|
var errorTests = 0;
|
|
1701
2279
|
var errorMessages = [];
|
|
1702
2280
|
var results = [];
|
|
1703
2281
|
var resultsByTickers = /* @__PURE__ */ new Map();
|
|
2282
|
+
var resultArtifactsByTestName = /* @__PURE__ */ new Map();
|
|
2283
|
+
var artifactRefCountsByTestName = /* @__PURE__ */ new Map();
|
|
2284
|
+
var bestTickerTestNameBySymbol = /* @__PURE__ */ new Map();
|
|
2285
|
+
var topResultNames = /* @__PURE__ */ new Set();
|
|
2286
|
+
var persistedTestSummaryByKey = /* @__PURE__ */ new Map();
|
|
1704
2287
|
var userName = flags.user;
|
|
1705
2288
|
var runStartedAt = Date.now();
|
|
2289
|
+
var testsStartedAt = runStartedAt;
|
|
1706
2290
|
var createListIt = () => new ListIt({
|
|
1707
2291
|
autoAlign: true,
|
|
1708
2292
|
headerUnderline: true
|
|
1709
2293
|
});
|
|
1710
2294
|
var createTable = (headers, rows) => createListIt().setHeaderRow(headers).d(rows).toString();
|
|
1711
2295
|
var createTimestamp = (date) => (0, import_date_fns.format)(date, "yyyyMMddHHmm");
|
|
1712
|
-
var
|
|
2296
|
+
var formatDuration = (startedAt) => {
|
|
2297
|
+
const seconds = (Date.now() - startedAt) / 1e3;
|
|
2298
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
2299
|
+
const minutes = Math.floor(seconds / 60);
|
|
2300
|
+
const restSeconds = Math.round(seconds % 60);
|
|
2301
|
+
return `${minutes}m ${restSeconds}s`;
|
|
2302
|
+
};
|
|
2303
|
+
var formatWindowDays = (startMs, endMs) => {
|
|
2304
|
+
const days = Math.max(0, (endMs - startMs) / (24 * 60 * 60 * 1e3));
|
|
2305
|
+
return Number.isInteger(days) ? String(days) : days.toFixed(2);
|
|
2306
|
+
};
|
|
2307
|
+
var timeOperation = async (label, operation) => {
|
|
2308
|
+
const startedAt = Date.now();
|
|
2309
|
+
try {
|
|
2310
|
+
return await operation();
|
|
2311
|
+
} finally {
|
|
2312
|
+
console.log(import_chalk2.default.gray(`${label}: done in ${formatDuration(startedAt)}`));
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
var isGoodTest = (result) => result.stat?.orders > 5 && result.stat?.profit > 10;
|
|
2316
|
+
var getResultAmount = (result) => result.stat.amount ?? 0;
|
|
1713
2317
|
var recordError = (error) => {
|
|
1714
2318
|
errorMessages.push(error);
|
|
1715
2319
|
};
|
|
2320
|
+
var stripInlineLogs = (result) => {
|
|
2321
|
+
const { inlineOrderLog, inlinePositionLog, ...resultWithoutLogs } = result;
|
|
2322
|
+
return resultWithoutLogs;
|
|
2323
|
+
};
|
|
2324
|
+
var getInlineArtifacts = (result) => {
|
|
2325
|
+
if (!result.inlineOrderLog && !result.inlinePositionLog) {
|
|
2326
|
+
return null;
|
|
2327
|
+
}
|
|
2328
|
+
return {
|
|
2329
|
+
orderLog: result.inlineOrderLog,
|
|
2330
|
+
positionLog: result.inlinePositionLog
|
|
2331
|
+
};
|
|
2332
|
+
};
|
|
2333
|
+
var retainArtifacts = (testName, artifacts) => {
|
|
2334
|
+
if (artifacts) {
|
|
2335
|
+
resultArtifactsByTestName.set(testName, artifacts);
|
|
2336
|
+
}
|
|
2337
|
+
artifactRefCountsByTestName.set(
|
|
2338
|
+
testName,
|
|
2339
|
+
(artifactRefCountsByTestName.get(testName) ?? 0) + 1
|
|
2340
|
+
);
|
|
2341
|
+
};
|
|
2342
|
+
var releaseArtifacts = (testName) => {
|
|
2343
|
+
const nextCount = (artifactRefCountsByTestName.get(testName) ?? 0) - 1;
|
|
2344
|
+
if (nextCount <= 0) {
|
|
2345
|
+
artifactRefCountsByTestName.delete(testName);
|
|
2346
|
+
resultArtifactsByTestName.delete(testName);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
artifactRefCountsByTestName.set(testName, nextCount);
|
|
2350
|
+
};
|
|
2351
|
+
var insertTopResult = (currentResults, nextResult, limit) => {
|
|
2352
|
+
if (limit <= 0) {
|
|
2353
|
+
return {
|
|
2354
|
+
results: [],
|
|
2355
|
+
added: false
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
const nextResults = [...currentResults];
|
|
2359
|
+
const nextAmount = getResultAmount(nextResult);
|
|
2360
|
+
let insertIndex = nextResults.length;
|
|
2361
|
+
for (let index = 0; index < nextResults.length; index += 1) {
|
|
2362
|
+
if (nextAmount > getResultAmount(nextResults[index])) {
|
|
2363
|
+
insertIndex = index;
|
|
2364
|
+
break;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
if (insertIndex === nextResults.length && nextResults.length >= limit) {
|
|
2368
|
+
return {
|
|
2369
|
+
results: currentResults,
|
|
2370
|
+
added: false
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
nextResults.splice(insertIndex, 0, nextResult);
|
|
2374
|
+
if (nextResults.length > limit) {
|
|
2375
|
+
nextResults.length = limit;
|
|
2376
|
+
}
|
|
2377
|
+
return {
|
|
2378
|
+
results: nextResults,
|
|
2379
|
+
added: nextResults.some(
|
|
2380
|
+
(candidate) => candidate.test.name === nextResult.test.name
|
|
2381
|
+
)
|
|
2382
|
+
};
|
|
2383
|
+
};
|
|
2384
|
+
var updateTopResults = (nextResults, nextResult, artifacts) => {
|
|
2385
|
+
const previousTopNames = topResultNames;
|
|
2386
|
+
const nextTopNames = new Set(nextResults.map((result) => result.test.name));
|
|
2387
|
+
if (artifacts && !previousTopNames.has(nextResult.test.name) && nextTopNames.has(nextResult.test.name)) {
|
|
2388
|
+
retainArtifacts(nextResult.test.name, artifacts);
|
|
2389
|
+
}
|
|
2390
|
+
for (const testName of previousTopNames) {
|
|
2391
|
+
if (!nextTopNames.has(testName)) {
|
|
2392
|
+
releaseArtifacts(testName);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
results = nextResults;
|
|
2396
|
+
topResultNames = nextTopNames;
|
|
2397
|
+
};
|
|
2398
|
+
var updateBestTickerResult = (result, artifacts) => {
|
|
2399
|
+
if (!isGoodTest(result)) {
|
|
2400
|
+
return false;
|
|
2401
|
+
}
|
|
2402
|
+
const previousResult = resultsByTickers.get(result.test.symbol);
|
|
2403
|
+
if (previousResult && previousResult.stat.profit >= result.stat.profit) {
|
|
2404
|
+
return false;
|
|
2405
|
+
}
|
|
2406
|
+
const previousTestName = bestTickerTestNameBySymbol.get(result.test.symbol);
|
|
2407
|
+
if (previousTestName) {
|
|
2408
|
+
releaseArtifacts(previousTestName);
|
|
2409
|
+
}
|
|
2410
|
+
resultsByTickers.set(result.test.symbol, result);
|
|
2411
|
+
bestTickerTestNameBySymbol.set(result.test.symbol, result.test.name);
|
|
2412
|
+
if (artifacts) {
|
|
2413
|
+
retainArtifacts(result.test.name, artifacts);
|
|
2414
|
+
}
|
|
2415
|
+
return true;
|
|
2416
|
+
};
|
|
1716
2417
|
var isStrategyConfigGrid = (value) => {
|
|
1717
2418
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1718
2419
|
return false;
|
|
@@ -1721,13 +2422,27 @@ var isStrategyConfigGrid = (value) => {
|
|
|
1721
2422
|
(item) => Array.isArray(item)
|
|
1722
2423
|
);
|
|
1723
2424
|
};
|
|
2425
|
+
var mergePersistedTestSummaries = (existing, persisted) => {
|
|
2426
|
+
const mergedByKey = /* @__PURE__ */ new Map();
|
|
2427
|
+
for (const item of existing || []) {
|
|
2428
|
+
const strategyName = typeof item?.data?.strategyName === "string" ? item.data.strategyName : "";
|
|
2429
|
+
if (!strategyName || typeof item?.value !== "string") {
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
mergedByKey.set(`${strategyName}:${item.value}`, item);
|
|
2433
|
+
}
|
|
2434
|
+
for (const [key, item] of persisted) {
|
|
2435
|
+
mergedByKey.set(key, item);
|
|
2436
|
+
}
|
|
2437
|
+
return [...mergedByKey.values()];
|
|
2438
|
+
};
|
|
1724
2439
|
var resolveBacktestConnectorName = async (value) => {
|
|
1725
2440
|
const connectorName = await (0, import_connectors2.resolveConnectorName)(value, projectRoot);
|
|
1726
2441
|
if (connectorName) {
|
|
1727
2442
|
return connectorName;
|
|
1728
2443
|
}
|
|
1729
2444
|
console.log(
|
|
1730
|
-
|
|
2445
|
+
import_chalk2.default.yellow(
|
|
1731
2446
|
`Unknown connector "${String(value || "").trim() || String(value)}". Fallback to ${import_connectors2.DEFAULT_CONNECTOR_NAME}.`
|
|
1732
2447
|
)
|
|
1733
2448
|
);
|
|
@@ -1735,33 +2450,79 @@ var resolveBacktestConnectorName = async (value) => {
|
|
|
1735
2450
|
};
|
|
1736
2451
|
var getLogsById = async (orderLogId) => {
|
|
1737
2452
|
const orderLog = await (0, import_redis.getData)(
|
|
1738
|
-
import_redis.redisKeys.cacheOrders(userName, orderLogId)
|
|
2453
|
+
import_redis.redisKeys.cacheOrders(userName, orderLogId),
|
|
2454
|
+
null
|
|
1739
2455
|
);
|
|
1740
2456
|
const positionLog = await (0, import_redis.getData)(
|
|
1741
|
-
import_redis.redisKeys.cachePositions(userName, orderLogId)
|
|
2457
|
+
import_redis.redisKeys.cachePositions(userName, orderLogId),
|
|
2458
|
+
null
|
|
1742
2459
|
);
|
|
1743
2460
|
return { orderLog, positionLog };
|
|
1744
2461
|
};
|
|
2462
|
+
var cacheArtifactsById = async (orderLogId, artifacts) => {
|
|
2463
|
+
if (!artifacts || !Array.isArray(artifacts.orderLog) || !Array.isArray(artifacts.positionLog)) {
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
await Promise.all([
|
|
2467
|
+
(0, import_redis.setData)(import_redis.redisKeys.cacheOrders(userName, orderLogId), artifacts.orderLog, {
|
|
2468
|
+
expire: import_constants2.TTL_1D
|
|
2469
|
+
}),
|
|
2470
|
+
(0, import_redis.setData)(
|
|
2471
|
+
import_redis.redisKeys.cachePositions(userName, orderLogId),
|
|
2472
|
+
artifacts.positionLog,
|
|
2473
|
+
{
|
|
2474
|
+
expire: import_constants2.TTL_1D
|
|
2475
|
+
}
|
|
2476
|
+
)
|
|
2477
|
+
]);
|
|
2478
|
+
};
|
|
2479
|
+
var resolveResultArtifacts = async (result) => {
|
|
2480
|
+
const inlineArtifacts = resultArtifactsByTestName.get(result.test.name);
|
|
2481
|
+
if (inlineArtifacts?.orderLog && inlineArtifacts?.positionLog) {
|
|
2482
|
+
return inlineArtifacts;
|
|
2483
|
+
}
|
|
2484
|
+
return getLogsById(result.orderLogId);
|
|
2485
|
+
};
|
|
1745
2486
|
var setTestData = async (test, stat, orderLog) => {
|
|
1746
2487
|
await (0, import_redis.setData)(
|
|
1747
2488
|
import_redis.redisKeys.testOrders(test.userName, test.strategyName, test.name),
|
|
1748
2489
|
orderLog,
|
|
1749
2490
|
{
|
|
1750
|
-
expire:
|
|
2491
|
+
expire: import_constants2.TTL_1M
|
|
1751
2492
|
}
|
|
1752
2493
|
);
|
|
1753
2494
|
await (0, import_redis.setData)(
|
|
1754
2495
|
import_redis.redisKeys.testConfig(test.userName, test.strategyName, test.name),
|
|
1755
2496
|
test,
|
|
1756
2497
|
{
|
|
1757
|
-
expire:
|
|
2498
|
+
expire: import_constants2.TTL_1M
|
|
1758
2499
|
}
|
|
1759
2500
|
);
|
|
1760
2501
|
await (0, import_redis.setData)(
|
|
1761
2502
|
import_redis.redisKeys.testStat(test.userName, test.strategyName, test.name),
|
|
1762
2503
|
stat,
|
|
1763
2504
|
{
|
|
1764
|
-
expire:
|
|
2505
|
+
expire: import_constants2.TTL_1M
|
|
2506
|
+
}
|
|
2507
|
+
);
|
|
2508
|
+
const { testId } = (0, import_backtest.parseTestName)(test.name);
|
|
2509
|
+
persistedTestSummaryByKey.set(`${test.strategyName}:${test.name}`, {
|
|
2510
|
+
value: test.name,
|
|
2511
|
+
label: `${test.symbol}_${testId}`,
|
|
2512
|
+
description: `${stat.netProfit || 0}$`,
|
|
2513
|
+
data: {
|
|
2514
|
+
netProfit: stat.netProfit || 0,
|
|
2515
|
+
strategyName: test.strategyName
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
};
|
|
2519
|
+
var persistTestSummariesIndex = async () => {
|
|
2520
|
+
const existing = await (0, import_redis.getData)(import_redis.redisKeys.testSummaries(userName), []);
|
|
2521
|
+
await (0, import_redis.setData)(
|
|
2522
|
+
import_redis.redisKeys.testSummaries(userName),
|
|
2523
|
+
mergePersistedTestSummaries(existing, persistedTestSummaryByKey),
|
|
2524
|
+
{
|
|
2525
|
+
expire: 0
|
|
1765
2526
|
}
|
|
1766
2527
|
);
|
|
1767
2528
|
};
|
|
@@ -1797,18 +2558,39 @@ var backtest = async () => {
|
|
|
1797
2558
|
const marketConnector = await connectorFactory({
|
|
1798
2559
|
userName: flags.user
|
|
1799
2560
|
});
|
|
1800
|
-
const tickers = await (
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
2561
|
+
const tickers = await timeOperation(
|
|
2562
|
+
"tickers load",
|
|
2563
|
+
() => (0, import_cli.getTickers)(
|
|
2564
|
+
marketConnector,
|
|
2565
|
+
flags.tickers,
|
|
2566
|
+
flags.exclude,
|
|
2567
|
+
flags.tickersLimit
|
|
2568
|
+
)
|
|
1805
2569
|
);
|
|
1806
2570
|
if (flags.showTickersList) {
|
|
1807
|
-
console.log(
|
|
2571
|
+
console.log(import_chalk2.default.gray(JSON.stringify(tickers.sort(), null, 2)));
|
|
1808
2572
|
return;
|
|
1809
2573
|
}
|
|
2574
|
+
const window2 = resolveTimeWindow({
|
|
2575
|
+
days: flags.days,
|
|
2576
|
+
startTime: flags.startTime,
|
|
2577
|
+
endTime: flags.endTime,
|
|
2578
|
+
defaultStartMs: (0, import_time.getTimestamp)(import_constants2.BACKTEST_DEFAULT_DAYS),
|
|
2579
|
+
defaultEndMs: (0, import_time.getTimestamp)()
|
|
2580
|
+
});
|
|
2581
|
+
const preloadStart = (0, import_time.getBacktestPreloadStart)(
|
|
2582
|
+
window2.start,
|
|
2583
|
+
import_constants2.BACKTEST_PRELOAD_DAYS
|
|
2584
|
+
);
|
|
1810
2585
|
if (!flags.cacheOnly) {
|
|
1811
|
-
await (
|
|
2586
|
+
await timeOperation(
|
|
2587
|
+
`update ${connectorName}`,
|
|
2588
|
+
() => (0, import_cli.update)(marketConnector, interval, tickers, void 0, {
|
|
2589
|
+
connectorLabel: connectorName,
|
|
2590
|
+
preloadStart,
|
|
2591
|
+
preloadEnd: window2.end
|
|
2592
|
+
})
|
|
2593
|
+
);
|
|
1812
2594
|
const binanceConnectorCreator = await (0, import_connectors2.getConnectorCreatorByName)(
|
|
1813
2595
|
import_connectors.ConnectorNames.Binance,
|
|
1814
2596
|
projectRoot
|
|
@@ -1826,8 +2608,22 @@ var backtest = async () => {
|
|
|
1826
2608
|
const coinbaseConnector = await coinbaseConnectorCreator({
|
|
1827
2609
|
userName: flags.user
|
|
1828
2610
|
});
|
|
1829
|
-
await (
|
|
1830
|
-
|
|
2611
|
+
await timeOperation(
|
|
2612
|
+
`update ${import_connectors.ConnectorNames.Binance}`,
|
|
2613
|
+
() => (0, import_cli.update)(binanceConnector, interval, ["BTCUSDT"], void 0, {
|
|
2614
|
+
connectorLabel: import_connectors.ConnectorNames.Binance,
|
|
2615
|
+
preloadStart,
|
|
2616
|
+
preloadEnd: window2.end
|
|
2617
|
+
})
|
|
2618
|
+
);
|
|
2619
|
+
await timeOperation(
|
|
2620
|
+
`update ${import_connectors.ConnectorNames.Coinbase}`,
|
|
2621
|
+
() => (0, import_cli.update)(coinbaseConnector, interval, ["BTCUSDT"], void 0, {
|
|
2622
|
+
connectorLabel: import_connectors.ConnectorNames.Coinbase,
|
|
2623
|
+
preloadStart,
|
|
2624
|
+
preloadEnd: window2.end
|
|
2625
|
+
})
|
|
2626
|
+
);
|
|
1831
2627
|
}
|
|
1832
2628
|
if (flags.updateOnly) {
|
|
1833
2629
|
return;
|
|
@@ -1838,15 +2634,59 @@ var backtest = async () => {
|
|
|
1838
2634
|
strategyName,
|
|
1839
2635
|
typedBacktestConfig,
|
|
1840
2636
|
connectorName
|
|
1841
|
-
)
|
|
2637
|
+
);
|
|
1842
2638
|
const mlEnabled = Boolean(flags.ml);
|
|
1843
|
-
|
|
1844
|
-
|
|
2639
|
+
const aiEnabled = Boolean(flags.ai);
|
|
2640
|
+
testSuite = testSuite.map((test) => ({
|
|
2641
|
+
...test,
|
|
2642
|
+
options: {
|
|
2643
|
+
start: window2.start,
|
|
2644
|
+
end: window2.end
|
|
2645
|
+
},
|
|
2646
|
+
ml: mlEnabled,
|
|
2647
|
+
ai: aiEnabled,
|
|
2648
|
+
timeoutMs: testItemTimeoutMs
|
|
2649
|
+
})).slice(testsSkip, testsSkip + testsLimit);
|
|
2650
|
+
if (!testSuite.length) {
|
|
2651
|
+
console.log(
|
|
2652
|
+
import_chalk2.default.yellow(
|
|
2653
|
+
`No tests selected (skip=${testsSkip}, limit=${testsLimit}).`
|
|
2654
|
+
)
|
|
2655
|
+
);
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
if (shouldBackfillDerivativesContextForBacktest({
|
|
2659
|
+
aiEnabled,
|
|
2660
|
+
cacheOnly: Boolean(flags.cacheOnly),
|
|
2661
|
+
mlEnabled
|
|
2662
|
+
})) {
|
|
2663
|
+
await timeOperation(
|
|
2664
|
+
"derivatives context backfill",
|
|
2665
|
+
() => backfillDerivativesContextForBacktest({
|
|
2666
|
+
userName,
|
|
2667
|
+
symbols: testSuite.map((test) => test.symbol),
|
|
2668
|
+
startMs: window2.start,
|
|
2669
|
+
endMs: window2.end,
|
|
2670
|
+
preloadStartMs: preloadStart
|
|
2671
|
+
})
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
testsStartedAt = Date.now();
|
|
2675
|
+
const chunkSize = Math.ceil(testSuite.length / effectiveParallel);
|
|
1845
2676
|
const chunks = import_lodash.default.chunk(testSuite, chunkSize);
|
|
1846
|
-
let completedWorkers = 0;
|
|
1847
2677
|
let completedTests = 0;
|
|
1848
2678
|
let isFinishing = false;
|
|
1849
2679
|
const workers = /* @__PURE__ */ new Set();
|
|
2680
|
+
const maybeFinish = async () => {
|
|
2681
|
+
if (isFinishing) {
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
if (completedTests !== testSuite.length || workers.size > 0) {
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
isFinishing = true;
|
|
2688
|
+
await finish();
|
|
2689
|
+
};
|
|
1850
2690
|
const stopWorkers = () => {
|
|
1851
2691
|
for (const worker of workers) {
|
|
1852
2692
|
if (!worker.killed) {
|
|
@@ -1862,9 +2702,22 @@ var backtest = async () => {
|
|
|
1862
2702
|
stopWorkers();
|
|
1863
2703
|
process.exit(143);
|
|
1864
2704
|
});
|
|
1865
|
-
console.log(
|
|
2705
|
+
console.log(import_chalk2.default.yellow(`tests: ${testSuite.length}`));
|
|
2706
|
+
console.log(
|
|
2707
|
+
import_chalk2.default.gray(`parallel: ${effectiveParallel}, workerHeapMb: ${workerHeapMb}`)
|
|
2708
|
+
);
|
|
2709
|
+
console.log(
|
|
2710
|
+
import_chalk2.default.gray(
|
|
2711
|
+
`window: ${(0, import_time.formatUnix)(window2.start)} -> ${(0, import_time.formatUnix)(window2.end)} (${formatWindowDays(window2.start, window2.end)}d, ${window2.source})`
|
|
2712
|
+
)
|
|
2713
|
+
);
|
|
2714
|
+
console.log(
|
|
2715
|
+
import_chalk2.default.gray(
|
|
2716
|
+
`preload: ${(0, import_time.formatUnix)(preloadStart)} -> ${(0, import_time.formatUnix)(window2.end)} (${import_constants2.BACKTEST_PRELOAD_DAYS}d warmup)`
|
|
2717
|
+
)
|
|
2718
|
+
);
|
|
1866
2719
|
console.log("");
|
|
1867
|
-
const bar = new
|
|
2720
|
+
const bar = new import_progress2.default(
|
|
1868
2721
|
":current/:total [:bar][:percent] :symbol :amount :eta(s)",
|
|
1869
2722
|
{
|
|
1870
2723
|
total: testSuite.length,
|
|
@@ -1876,23 +2729,18 @@ var backtest = async () => {
|
|
|
1876
2729
|
const chunkWithId = chunk.map((test) => ({ ...test, chunkId }));
|
|
1877
2730
|
const tester = (0, import_child_process.fork)(testerWorkerPath, [], {
|
|
1878
2731
|
execArgv: testerNeedsTsRuntime ? [
|
|
1879
|
-
|
|
2732
|
+
`--max-old-space-size=${workerHeapMb}`,
|
|
1880
2733
|
"-r",
|
|
1881
2734
|
"ts-node/register",
|
|
1882
2735
|
"-r",
|
|
1883
2736
|
"tsconfig-paths/register"
|
|
1884
|
-
] : [
|
|
2737
|
+
] : [`--max-old-space-size=${workerHeapMb}`]
|
|
1885
2738
|
});
|
|
1886
2739
|
workers.add(tester);
|
|
1887
2740
|
tester.on("message", async (msg) => {
|
|
1888
2741
|
if (msg.done) {
|
|
1889
|
-
completedWorkers++;
|
|
1890
2742
|
workers.delete(tester);
|
|
1891
|
-
|
|
1892
|
-
isFinishing = true;
|
|
1893
|
-
results = (0, import_backtest.sortBestTests)(results, flags.top);
|
|
1894
|
-
await finish();
|
|
1895
|
-
}
|
|
2743
|
+
await maybeFinish();
|
|
1896
2744
|
return;
|
|
1897
2745
|
}
|
|
1898
2746
|
completedTests++;
|
|
@@ -1904,49 +2752,55 @@ var backtest = async () => {
|
|
|
1904
2752
|
payload: msg
|
|
1905
2753
|
});
|
|
1906
2754
|
console.error(
|
|
1907
|
-
|
|
2755
|
+
import_chalk2.default.red(`Error in test #${msg.id}: ${JSON.stringify(msg)}`)
|
|
1908
2756
|
);
|
|
1909
2757
|
return;
|
|
1910
2758
|
} else {
|
|
1911
2759
|
successTests++;
|
|
1912
2760
|
}
|
|
1913
|
-
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
}
|
|
2761
|
+
const fullResult = msg;
|
|
2762
|
+
const result = stripInlineLogs(fullResult);
|
|
2763
|
+
const inlineArtifacts = getInlineArtifacts(fullResult);
|
|
2764
|
+
const nextTopResults = insertTopResult(results, result, flags.top);
|
|
2765
|
+
const shouldCacheTopArtifacts = nextTopResults.added;
|
|
2766
|
+
if (nextTopResults.added || results !== nextTopResults.results) {
|
|
2767
|
+
updateTopResults(nextTopResults.results, result, inlineArtifacts);
|
|
2768
|
+
}
|
|
2769
|
+
const updatedBestTickerResult = updateBestTickerResult(
|
|
2770
|
+
result,
|
|
2771
|
+
inlineArtifacts
|
|
2772
|
+
);
|
|
2773
|
+
if ((shouldCacheTopArtifacts || updatedBestTickerResult) && inlineArtifacts) {
|
|
2774
|
+
await cacheArtifactsById(result.orderLogId, inlineArtifacts);
|
|
2775
|
+
}
|
|
1921
2776
|
if (completedTests % progressStep === 0 || completedTests === testSuite.length) {
|
|
1922
|
-
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1925
|
-
stat: { profit }
|
|
1926
|
-
} = results[0];
|
|
2777
|
+
const bestResult = results[0];
|
|
2778
|
+
const symbol = bestResult?.test.symbol || "-";
|
|
2779
|
+
const profit = bestResult?.stat.profit || 0;
|
|
1927
2780
|
const profitStr = `${(profit || 0).toFixed(2)}$`;
|
|
1928
2781
|
bar.tick(
|
|
1929
2782
|
completedTests === testSuite.length ? completedTests % progressStep : progressStep,
|
|
1930
2783
|
{
|
|
1931
|
-
symbol:
|
|
1932
|
-
amount: profit > 0 ?
|
|
2784
|
+
symbol: import_chalk2.default.yellow(symbol),
|
|
2785
|
+
amount: profit > 0 ? import_chalk2.default.green(profitStr) : import_chalk2.default.red(profitStr)
|
|
1933
2786
|
}
|
|
1934
2787
|
);
|
|
1935
2788
|
}
|
|
1936
2789
|
});
|
|
1937
2790
|
tester.on("error", (err) => {
|
|
1938
2791
|
recordError({ error: err?.message ?? err });
|
|
1939
|
-
console.error(
|
|
2792
|
+
console.error(import_chalk2.default.red(`Worker error: ${err.message}`));
|
|
1940
2793
|
});
|
|
1941
2794
|
tester.on("exit", (code) => {
|
|
1942
2795
|
workers.delete(tester);
|
|
1943
2796
|
if (code !== 0) {
|
|
1944
2797
|
recordError({ error: `Worker exited with code ${code}` });
|
|
1945
|
-
console.error(
|
|
2798
|
+
console.error(import_chalk2.default.red(`Worker exited with code ${code}`));
|
|
1946
2799
|
}
|
|
2800
|
+
void maybeFinish();
|
|
1947
2801
|
});
|
|
1948
2802
|
await (0, import_redis.setData)(import_redis.redisKeys.cacheChunk(userName, chunkId), chunkWithId, {
|
|
1949
|
-
expire:
|
|
2803
|
+
expire: import_constants2.TTL_1D
|
|
1950
2804
|
});
|
|
1951
2805
|
tester.send({ chunkId, userName });
|
|
1952
2806
|
}
|
|
@@ -1954,14 +2808,22 @@ var backtest = async () => {
|
|
|
1954
2808
|
var saveAndPrintResults = async () => {
|
|
1955
2809
|
const colorizedResults = [];
|
|
1956
2810
|
for await (const result of results) {
|
|
1957
|
-
const { test
|
|
2811
|
+
const { test } = result;
|
|
1958
2812
|
const { symbol, name } = test;
|
|
1959
|
-
const { orderLog, positionLog } = await
|
|
2813
|
+
const { orderLog, positionLog } = await resolveResultArtifacts(result);
|
|
2814
|
+
if (!orderLog || !positionLog) {
|
|
2815
|
+
throw new Error(`Logs not found for test ${name}`);
|
|
2816
|
+
}
|
|
1960
2817
|
const stat = (0, import_backtest.calculateStatsFull)(positionLog);
|
|
2818
|
+
if (!stat && result.stat.orders > 0) {
|
|
2819
|
+
throw new Error(
|
|
2820
|
+
`Position log is empty for test ${name} despite ${result.stat.orders} closed orders`
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
1961
2823
|
await setTestData(test, stat, orderLog);
|
|
1962
2824
|
const statRow = [
|
|
1963
|
-
|
|
1964
|
-
|
|
2825
|
+
import_chalk2.default.blue(name),
|
|
2826
|
+
import_chalk2.default.yellow(symbol),
|
|
1965
2827
|
...(0, import_cli.drawStatInCLI)(stat, [
|
|
1966
2828
|
"netProfit",
|
|
1967
2829
|
"orders",
|
|
@@ -1980,14 +2842,22 @@ var saveAndPrintResults = async () => {
|
|
|
1980
2842
|
var saveAndPrintResultsByTickers = async () => {
|
|
1981
2843
|
const colorizedResultsByTickers = [];
|
|
1982
2844
|
for await (const result of resultsByTickers.values()) {
|
|
1983
|
-
const { test
|
|
2845
|
+
const { test } = result;
|
|
1984
2846
|
const { symbol, name } = test;
|
|
1985
|
-
const { orderLog, positionLog } = await
|
|
2847
|
+
const { orderLog, positionLog } = await resolveResultArtifacts(result);
|
|
2848
|
+
if (!orderLog || !positionLog) {
|
|
2849
|
+
throw new Error(`Logs not found for ticker result ${name}`);
|
|
2850
|
+
}
|
|
1986
2851
|
const stat = (0, import_backtest.calculateStatsFull)(positionLog);
|
|
2852
|
+
if (!stat && result.stat.orders > 0) {
|
|
2853
|
+
throw new Error(
|
|
2854
|
+
`Position log is empty for ticker result ${name} despite ${result.stat.orders} closed orders`
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
1987
2857
|
await setTestData(test, stat, orderLog);
|
|
1988
2858
|
const statRow = [
|
|
1989
|
-
|
|
1990
|
-
|
|
2859
|
+
import_chalk2.default.blue(name),
|
|
2860
|
+
import_chalk2.default.yellow(symbol),
|
|
1991
2861
|
...(0, import_cli.drawStatInCLI)(stat, [
|
|
1992
2862
|
"netProfit",
|
|
1993
2863
|
"orders",
|
|
@@ -2011,18 +2881,26 @@ var finish = async () => {
|
|
|
2011
2881
|
} else {
|
|
2012
2882
|
await saveAndPrintResultsByTickers();
|
|
2013
2883
|
}
|
|
2884
|
+
await persistTestSummariesIndex();
|
|
2885
|
+
console.log(
|
|
2886
|
+
import_chalk2.default.gray(`tests run: done in ${formatDuration(testsStartedAt)}`)
|
|
2887
|
+
);
|
|
2888
|
+
console.log(
|
|
2889
|
+
import_chalk2.default.gray(`backtest total: done in ${formatDuration(runStartedAt)}`)
|
|
2890
|
+
);
|
|
2891
|
+
console.log("");
|
|
2014
2892
|
const bestConfig = results[0]?.test.strategyConfig;
|
|
2015
|
-
console.log(
|
|
2016
|
-
console.log(
|
|
2893
|
+
console.log(import_chalk2.default.gray("BEST CONFIG:"));
|
|
2894
|
+
console.log(import_chalk2.default.green((0, import_data.toJson)(bestConfig, true)));
|
|
2017
2895
|
console.log("");
|
|
2018
2896
|
const mergedConfig = (0, import_backtest.mergeConfigs)(
|
|
2019
2897
|
results.map(({ test: { strategyConfig } }) => strategyConfig)
|
|
2020
2898
|
);
|
|
2021
|
-
console.log(
|
|
2022
|
-
console.log(
|
|
2899
|
+
console.log(import_chalk2.default.gray("MERGED CONFIG:"));
|
|
2900
|
+
console.log(import_chalk2.default.blue((0, import_data.toJson)(mergedConfig, true)));
|
|
2023
2901
|
console.log("");
|
|
2024
|
-
console.log(`${
|
|
2025
|
-
console.log(`${
|
|
2902
|
+
console.log(`${import_chalk2.default.green("SUCCESS TESTS")}: ${successTests}`);
|
|
2903
|
+
console.log(`${import_chalk2.default.red("ERRORS")}: ${errorTests}`);
|
|
2026
2904
|
console.log("");
|
|
2027
2905
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
2028
2906
|
const durationSeconds = Number(
|
|
@@ -2038,7 +2916,7 @@ var finish = async () => {
|
|
|
2038
2916
|
finishedAt: finishedAt.toISOString(),
|
|
2039
2917
|
durationSeconds,
|
|
2040
2918
|
results,
|
|
2041
|
-
resultsByTickers: resultsByTickers.values(),
|
|
2919
|
+
resultsByTickers: Array.from(resultsByTickers.values()),
|
|
2042
2920
|
bestConfig,
|
|
2043
2921
|
mergedConfig,
|
|
2044
2922
|
successTests,
|
|
@@ -2051,4 +2929,13 @@ var finish = async () => {
|
|
|
2051
2929
|
);
|
|
2052
2930
|
process.exit();
|
|
2053
2931
|
};
|
|
2054
|
-
|
|
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
|
+
});
|