@tradejs/cli 1.0.8 → 1.0.10
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 +29748 -13641
- package/dist/lib/aiPocketSearch.js +1316 -0
- package/dist/lib/aiTrainCharts.js +711 -0
- package/dist/lib/aiTrainDataset.js +40 -0
- package/dist/lib/aiTrainEvaluationDump.js +76 -0
- package/dist/lib/aiTrainMetrics.js +148 -8
- package/dist/lib/aiTrainOptions.js +130 -0
- package/dist/lib/aiTrainQuarantine.js +199 -0
- package/dist/lib/aiTrainResearch.js +467 -0
- package/dist/lib/binanceMarketContextBackfill.js +841 -0
- package/dist/lib/binanceMarketData.js +392 -0
- package/dist/lib/coinMarketCapContextBackfill.js +1088 -0
- package/dist/lib/continuity.js +60 -0
- package/dist/lib/derivativesContextBackfill.js +490 -77
- package/dist/lib/executionCalibration.js +705 -0
- package/dist/lib/marketContextPrepare.js +4403 -0
- package/dist/lib/paritySummary.js +101 -0
- package/dist/lib/runBot.js +1 -0
- package/dist/lib/runEnvironment.js +2254 -0
- package/dist/lib/runFormatting.js +1657 -0
- package/dist/lib/runtimeDebugEvidence.js +526 -0
- package/dist/lib/runtimeModeConfig.js +53 -0
- package/dist/lib/runtimeParity.js +88 -11
- package/dist/lib/runtimeParityDetails.js +751 -0
- package/dist/lib/runtimeRedis.js +157 -0
- package/dist/lib/runtimeSignalsLoader.js +187 -0
- package/dist/lib/runtimeSignalsStorage.js +185 -0
- package/dist/lib/runtimeStrategyBacktest.js +134 -0
- package/dist/lib/runtimeTradeSync.js +263 -0
- package/dist/lib/telegramReports.js +9 -0
- package/dist/lib/tickerUniverseCache.js +116 -0
- package/dist/lib/timeWindow.js +3 -3
- package/dist/scripts/agentRun.js +28 -8
- package/dist/scripts/aiExport.js +1721 -7
- package/dist/scripts/aiExportSelect.js +16 -4
- package/dist/scripts/aiPocketSearch.js +4190 -0
- package/dist/scripts/aiTrain.js +1866 -87
- package/dist/scripts/backtest.js +4509 -818
- package/dist/scripts/binanceMarketIngest.js +565 -0
- package/dist/scripts/bot.js +16 -2
- package/dist/scripts/candlesMigrateProvider.js +32 -22
- package/dist/scripts/cleanDir.js +15 -2
- package/dist/scripts/cleanRedis.js +15 -2
- package/dist/scripts/cleanTests.js +38 -2
- package/dist/scripts/cleanupMarketContext.js +69 -0
- package/dist/scripts/continuity.js +148 -67
- package/dist/scripts/derivativesIngest.js +14 -4
- package/dist/scripts/derivativesIngestCoinalyzeAll.js +14 -4
- package/dist/scripts/doctor.js +14 -4
- package/dist/scripts/executionCalibration.js +792 -0
- package/dist/scripts/infraCommon.js +10 -0
- package/dist/scripts/infraDown.js +14 -4
- package/dist/scripts/infraInit.js +24 -4
- package/dist/scripts/infraUp.js +14 -4
- package/dist/scripts/marketWs.js +222 -0
- package/dist/scripts/migration.js +15 -2
- package/dist/scripts/mlExport.js +1698 -10
- package/dist/scripts/mlExportSelect.js +16 -4
- package/dist/scripts/mlInspect.js +13 -3
- package/dist/scripts/mlTrainLatestSelect.js +14 -4
- package/dist/scripts/replay.js +8824 -0
- package/dist/scripts/replayRunner.js +8827 -0
- package/dist/scripts/replayRuntimeEvidence.js +868 -0
- package/dist/scripts/researchAuto.js +289 -60
- package/dist/scripts/results.js +3 -3
- package/dist/scripts/runtimeEvidence.js +605 -0
- package/dist/scripts/runtimeParity.js +6360 -1004
- package/dist/scripts/serverHealth.js +588 -0
- package/dist/scripts/signals.js +5623 -486
- package/dist/scripts/signalsDaemon.js +6167 -0
- package/dist/scripts/signalsSummary.js +954 -312
- package/dist/scripts/test-ml.js +25 -3
- package/dist/scripts/test.js +26 -1
- package/dist/scripts/user-add.js +15 -2
- package/dist/workers/testerWorker.js +130 -21
- package/package.json +18 -12
package/dist/scripts/aiTrain.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({
|
|
@@ -1622,15 +1627,21 @@ var require_list_it = __commonJS({
|
|
|
1622
1627
|
});
|
|
1623
1628
|
|
|
1624
1629
|
// src/scripts/aiTrain.ts
|
|
1630
|
+
var aiTrain_exports = {};
|
|
1631
|
+
__export(aiTrain_exports, {
|
|
1632
|
+
main: () => main
|
|
1633
|
+
});
|
|
1634
|
+
module.exports = __toCommonJS(aiTrain_exports);
|
|
1625
1635
|
var import_args = __toESM(require("args"));
|
|
1626
1636
|
var import_chalk = __toESM(require("chalk"));
|
|
1627
|
-
var
|
|
1637
|
+
var import_promises2 = __toESM(require("fs/promises"));
|
|
1628
1638
|
var import_path = __toESM(require("path"));
|
|
1629
1639
|
var import_progress = __toESM(require("progress"));
|
|
1630
|
-
var
|
|
1640
|
+
var import_constants = require("@tradejs/core/constants");
|
|
1631
1641
|
var import_ai = require("@tradejs/infra/ai");
|
|
1642
|
+
var import_redis = require("@tradejs/infra/redis");
|
|
1632
1643
|
var import_ai2 = require("@tradejs/node/ai");
|
|
1633
|
-
var
|
|
1644
|
+
var import_constants2 = require("@tradejs/node/constants");
|
|
1634
1645
|
|
|
1635
1646
|
// src/lib/aiTrainMetrics.ts
|
|
1636
1647
|
var divideOrNull = (num, denom) => {
|
|
@@ -1643,17 +1654,119 @@ var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
|
1643
1654
|
var DAYS_PER_WEEK = 7;
|
|
1644
1655
|
var DAYS_PER_MONTH = 30.4375;
|
|
1645
1656
|
var getEvaluationPeriodDays = (evaluations) => {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1657
|
+
let minTimestamp = null;
|
|
1658
|
+
let maxTimestamp = null;
|
|
1659
|
+
for (const evaluation of evaluations) {
|
|
1660
|
+
const timestamp = typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp) ? evaluation.timestamp : null;
|
|
1661
|
+
if (timestamp == null) {
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
if (minTimestamp == null || timestamp < minTimestamp) {
|
|
1665
|
+
minTimestamp = timestamp;
|
|
1666
|
+
}
|
|
1667
|
+
if (maxTimestamp == null || timestamp > maxTimestamp) {
|
|
1668
|
+
maxTimestamp = timestamp;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
if (minTimestamp == null || maxTimestamp == null) {
|
|
1650
1672
|
return null;
|
|
1651
1673
|
}
|
|
1652
|
-
const minTimestamp = Math.min(...timestamps);
|
|
1653
|
-
const maxTimestamp = Math.max(...timestamps);
|
|
1654
1674
|
return Math.max((maxTimestamp - minTimestamp) / DAY_MS, 1);
|
|
1655
1675
|
};
|
|
1656
1676
|
var qualitySortKey = (quality) => quality == null ? Number.POSITIVE_INFINITY : quality;
|
|
1677
|
+
var emptyRiskSummary = () => ({
|
|
1678
|
+
trades: 0,
|
|
1679
|
+
totalProfit: 0,
|
|
1680
|
+
grossProfit: 0,
|
|
1681
|
+
grossLoss: 0,
|
|
1682
|
+
profitFactor: null,
|
|
1683
|
+
payoffRatio: null,
|
|
1684
|
+
avgWin: null,
|
|
1685
|
+
avgLoss: null,
|
|
1686
|
+
largestWin: null,
|
|
1687
|
+
largestLoss: null,
|
|
1688
|
+
winRate: null,
|
|
1689
|
+
maxDrawdown: 0,
|
|
1690
|
+
maxDrawdownPctOfGrossProfit: null,
|
|
1691
|
+
maxDrawdownPctOfTotalProfit: null,
|
|
1692
|
+
recoveryFactor: null,
|
|
1693
|
+
ulcerIndex: null,
|
|
1694
|
+
maxConsecutiveWins: 0,
|
|
1695
|
+
maxConsecutiveLosses: 0
|
|
1696
|
+
});
|
|
1697
|
+
var summarizeApprovedRisk = (evaluations) => {
|
|
1698
|
+
const approvedEvaluations = evaluations.filter((evaluation) => evaluation.aiApproved).sort((left, right) => {
|
|
1699
|
+
const leftTimestamp = typeof left.timestamp === "number" && Number.isFinite(left.timestamp) ? left.timestamp : Number.POSITIVE_INFINITY;
|
|
1700
|
+
const rightTimestamp = typeof right.timestamp === "number" && Number.isFinite(right.timestamp) ? right.timestamp : Number.POSITIVE_INFINITY;
|
|
1701
|
+
return leftTimestamp - rightTimestamp;
|
|
1702
|
+
});
|
|
1703
|
+
if (!approvedEvaluations.length) {
|
|
1704
|
+
return emptyRiskSummary();
|
|
1705
|
+
}
|
|
1706
|
+
let grossProfit = 0;
|
|
1707
|
+
let grossLoss = 0;
|
|
1708
|
+
let wins = 0;
|
|
1709
|
+
let losses = 0;
|
|
1710
|
+
let largestWin = null;
|
|
1711
|
+
let largestLoss = null;
|
|
1712
|
+
let equity = 0;
|
|
1713
|
+
let peakEquity = 0;
|
|
1714
|
+
let maxDrawdown = 0;
|
|
1715
|
+
let drawdownSquares = 0;
|
|
1716
|
+
let currentWinStreak = 0;
|
|
1717
|
+
let currentLossStreak = 0;
|
|
1718
|
+
let maxConsecutiveWins = 0;
|
|
1719
|
+
let maxConsecutiveLosses = 0;
|
|
1720
|
+
for (const evaluation of approvedEvaluations) {
|
|
1721
|
+
const profit = evaluation.profit;
|
|
1722
|
+
if (profit > 0) {
|
|
1723
|
+
grossProfit += profit;
|
|
1724
|
+
wins += 1;
|
|
1725
|
+
largestWin = largestWin == null ? profit : Math.max(largestWin, profit);
|
|
1726
|
+
currentWinStreak += 1;
|
|
1727
|
+
currentLossStreak = 0;
|
|
1728
|
+
} else if (profit < 0) {
|
|
1729
|
+
grossLoss += Math.abs(profit);
|
|
1730
|
+
losses += 1;
|
|
1731
|
+
largestLoss = largestLoss == null ? profit : Math.min(largestLoss, profit);
|
|
1732
|
+
currentLossStreak += 1;
|
|
1733
|
+
currentWinStreak = 0;
|
|
1734
|
+
} else {
|
|
1735
|
+
currentWinStreak = 0;
|
|
1736
|
+
currentLossStreak = 0;
|
|
1737
|
+
}
|
|
1738
|
+
maxConsecutiveWins = Math.max(maxConsecutiveWins, currentWinStreak);
|
|
1739
|
+
maxConsecutiveLosses = Math.max(maxConsecutiveLosses, currentLossStreak);
|
|
1740
|
+
equity += profit;
|
|
1741
|
+
peakEquity = Math.max(peakEquity, equity);
|
|
1742
|
+
const drawdown = Math.max(0, peakEquity - equity);
|
|
1743
|
+
maxDrawdown = Math.max(maxDrawdown, drawdown);
|
|
1744
|
+
drawdownSquares += drawdown * drawdown;
|
|
1745
|
+
}
|
|
1746
|
+
const totalProfit = grossProfit - grossLoss;
|
|
1747
|
+
const avgWin = divideOrNull(grossProfit, wins);
|
|
1748
|
+
const avgLoss = divideOrNull(grossLoss, losses);
|
|
1749
|
+
return {
|
|
1750
|
+
trades: approvedEvaluations.length,
|
|
1751
|
+
totalProfit,
|
|
1752
|
+
grossProfit,
|
|
1753
|
+
grossLoss,
|
|
1754
|
+
profitFactor: grossLoss > 0 ? grossProfit / grossLoss : null,
|
|
1755
|
+
payoffRatio: avgWin != null && avgLoss != null && avgLoss > 0 ? avgWin / avgLoss : null,
|
|
1756
|
+
avgWin,
|
|
1757
|
+
avgLoss,
|
|
1758
|
+
largestWin,
|
|
1759
|
+
largestLoss,
|
|
1760
|
+
winRate: divideOrNull(wins, approvedEvaluations.length),
|
|
1761
|
+
maxDrawdown,
|
|
1762
|
+
maxDrawdownPctOfGrossProfit: grossProfit > 0 ? maxDrawdown / grossProfit : null,
|
|
1763
|
+
maxDrawdownPctOfTotalProfit: totalProfit > 0 ? maxDrawdown / totalProfit : null,
|
|
1764
|
+
recoveryFactor: maxDrawdown > 0 ? totalProfit / maxDrawdown : null,
|
|
1765
|
+
ulcerIndex: Math.sqrt(drawdownSquares / approvedEvaluations.length),
|
|
1766
|
+
maxConsecutiveWins,
|
|
1767
|
+
maxConsecutiveLosses
|
|
1768
|
+
};
|
|
1769
|
+
};
|
|
1657
1770
|
var summarizeAiTrainEvaluations = (evaluations) => {
|
|
1658
1771
|
let approved = 0;
|
|
1659
1772
|
let rejected = 0;
|
|
@@ -1746,6 +1859,7 @@ var summarizeAiTrainEvaluations = (evaluations) => {
|
|
|
1746
1859
|
avgApprovedTradesPerDay,
|
|
1747
1860
|
avgApprovedTradesPerWeek,
|
|
1748
1861
|
expectancyDelta,
|
|
1862
|
+
approvedRisk: summarizeApprovedRisk(evaluations),
|
|
1749
1863
|
qualityBuckets: [...bucketMap.values()].sort(
|
|
1750
1864
|
(a, b) => qualitySortKey(a.quality) - qualitySortKey(b.quality)
|
|
1751
1865
|
)
|
|
@@ -1760,6 +1874,16 @@ var getDirectionSortKey = (direction) => {
|
|
|
1760
1874
|
}
|
|
1761
1875
|
return 2;
|
|
1762
1876
|
};
|
|
1877
|
+
var getMonthKey = (timestamp) => {
|
|
1878
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
|
|
1879
|
+
return "UNKNOWN";
|
|
1880
|
+
}
|
|
1881
|
+
return new Date(timestamp).toISOString().slice(0, 7);
|
|
1882
|
+
};
|
|
1883
|
+
var isDirectionMatchAtThreshold = (evaluation, threshold) => {
|
|
1884
|
+
const directionMatches = typeof evaluation.modelDirectionMatches === "boolean" ? evaluation.modelDirectionMatches : evaluation.aiApproved;
|
|
1885
|
+
return directionMatches === true && evaluation.quality != null && evaluation.quality >= threshold;
|
|
1886
|
+
};
|
|
1763
1887
|
var summarizeAiTrainEvaluationsByDirection = (evaluations) => {
|
|
1764
1888
|
const grouped = /* @__PURE__ */ new Map();
|
|
1765
1889
|
for (const evaluation of evaluations) {
|
|
@@ -1775,6 +1899,984 @@ var summarizeAiTrainEvaluationsByDirection = (evaluations) => {
|
|
|
1775
1899
|
summary: summarizeAiTrainEvaluations(directionEvaluations)
|
|
1776
1900
|
}));
|
|
1777
1901
|
};
|
|
1902
|
+
var summarizeAiTrainEvaluationsByMonth = (evaluations) => {
|
|
1903
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1904
|
+
for (const evaluation of evaluations) {
|
|
1905
|
+
const month = getMonthKey(evaluation.timestamp);
|
|
1906
|
+
const bucket = grouped.get(month) ?? [];
|
|
1907
|
+
bucket.push(evaluation);
|
|
1908
|
+
grouped.set(month, bucket);
|
|
1909
|
+
}
|
|
1910
|
+
return [...grouped.entries()].sort(([leftMonth], [rightMonth]) => leftMonth.localeCompare(rightMonth)).map(([month, monthEvaluations]) => ({
|
|
1911
|
+
month,
|
|
1912
|
+
summary: summarizeAiTrainEvaluations(monthEvaluations)
|
|
1913
|
+
}));
|
|
1914
|
+
};
|
|
1915
|
+
var summarizeAiTrainEvaluationsByQualityThreshold = (evaluations, thresholds = [3, 4, 5]) => thresholds.map((threshold) => ({
|
|
1916
|
+
threshold,
|
|
1917
|
+
label: `q${threshold}+`,
|
|
1918
|
+
summary: summarizeAiTrainEvaluations(
|
|
1919
|
+
evaluations.map((evaluation) => ({
|
|
1920
|
+
...evaluation,
|
|
1921
|
+
aiApproved: isDirectionMatchAtThreshold(evaluation, threshold)
|
|
1922
|
+
}))
|
|
1923
|
+
)
|
|
1924
|
+
}));
|
|
1925
|
+
|
|
1926
|
+
// src/lib/aiTrainCharts.ts
|
|
1927
|
+
var formatRatio = (value) => value == null ? "n/a" : `${(value * 100).toFixed(1)}%`;
|
|
1928
|
+
var formatPercent = (value) => value == null ? "n/a" : `${(value * 100).toFixed(1)}%`;
|
|
1929
|
+
var formatPercentValue = (value) => value == null ? "n/a" : `${value.toFixed(1)}%`;
|
|
1930
|
+
var formatSigned = (value) => value == null ? "n/a" : `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
|
|
1931
|
+
var formatNumber = (value) => value == null ? "n/a" : value.toFixed(2);
|
|
1932
|
+
var formatUtcWindowTimestamp = (timestamp) => {
|
|
1933
|
+
if (timestamp == null || !Number.isFinite(timestamp)) {
|
|
1934
|
+
return "n/a";
|
|
1935
|
+
}
|
|
1936
|
+
return new Date(timestamp).toISOString().replace("T", " ").replace(".000Z", " UTC");
|
|
1937
|
+
};
|
|
1938
|
+
var resolveMetricTone = (value) => {
|
|
1939
|
+
if (value == null) {
|
|
1940
|
+
return "default";
|
|
1941
|
+
}
|
|
1942
|
+
if (value > 0) {
|
|
1943
|
+
return "success";
|
|
1944
|
+
}
|
|
1945
|
+
if (value < 0) {
|
|
1946
|
+
return "error";
|
|
1947
|
+
}
|
|
1948
|
+
return "neutral";
|
|
1949
|
+
};
|
|
1950
|
+
var buildAiChartMetrics = (params) => {
|
|
1951
|
+
const { summary, maxDrawdownPercent } = params;
|
|
1952
|
+
const metrics = [
|
|
1953
|
+
{
|
|
1954
|
+
id: "maxDrawdown",
|
|
1955
|
+
label: "Max drawdown",
|
|
1956
|
+
value: formatPercentValue(maxDrawdownPercent),
|
|
1957
|
+
tone: maxDrawdownPercent == null ? "default" : maxDrawdownPercent > 0 ? "warning" : "success"
|
|
1958
|
+
},
|
|
1959
|
+
{
|
|
1960
|
+
id: "approved",
|
|
1961
|
+
label: "Approved",
|
|
1962
|
+
value: String(summary.approved)
|
|
1963
|
+
},
|
|
1964
|
+
{
|
|
1965
|
+
id: "accuracy",
|
|
1966
|
+
label: "Accuracy",
|
|
1967
|
+
value: formatPercent(
|
|
1968
|
+
summary.correct + summary.incorrect > 0 ? summary.correct / (summary.correct + summary.incorrect) : null
|
|
1969
|
+
)
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
id: "precision",
|
|
1973
|
+
label: "Precision",
|
|
1974
|
+
value: formatPercent(summary.precisionApproved)
|
|
1975
|
+
},
|
|
1976
|
+
{
|
|
1977
|
+
id: "recall",
|
|
1978
|
+
label: "Recall",
|
|
1979
|
+
value: formatPercent(summary.recallWinners)
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
id: "pnl",
|
|
1983
|
+
label: "P&L",
|
|
1984
|
+
value: formatSigned(summary.approvedRisk.totalProfit),
|
|
1985
|
+
tone: resolveMetricTone(summary.approvedRisk.totalProfit)
|
|
1986
|
+
},
|
|
1987
|
+
{
|
|
1988
|
+
id: "monthlyPnl",
|
|
1989
|
+
label: "Monthly P&L",
|
|
1990
|
+
value: formatSigned(summary.avgProfitApprovedPerMonth),
|
|
1991
|
+
tone: resolveMetricTone(summary.avgProfitApprovedPerMonth)
|
|
1992
|
+
},
|
|
1993
|
+
{
|
|
1994
|
+
id: "approvedPerDay",
|
|
1995
|
+
label: "Approved/Day",
|
|
1996
|
+
value: formatNumber(summary.avgApprovedTradesPerDay)
|
|
1997
|
+
},
|
|
1998
|
+
{
|
|
1999
|
+
id: "avgProfit",
|
|
2000
|
+
label: "Avg Profit",
|
|
2001
|
+
value: formatSigned(summary.avgProfitApproved),
|
|
2002
|
+
tone: resolveMetricTone(summary.avgProfitApproved)
|
|
2003
|
+
}
|
|
2004
|
+
];
|
|
2005
|
+
return metrics;
|
|
2006
|
+
};
|
|
2007
|
+
var buildAiChartDetails = (params) => {
|
|
2008
|
+
const { summary, rows } = params;
|
|
2009
|
+
let windowStart = null;
|
|
2010
|
+
let windowEnd = null;
|
|
2011
|
+
for (const row of rows) {
|
|
2012
|
+
const timestamp = row.timestamp != null && Number.isFinite(row.timestamp) ? row.timestamp : null;
|
|
2013
|
+
if (timestamp == null) {
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
if (windowStart == null || timestamp < windowStart) {
|
|
2017
|
+
windowStart = timestamp;
|
|
2018
|
+
}
|
|
2019
|
+
if (windowEnd == null || timestamp > windowEnd) {
|
|
2020
|
+
windowEnd = timestamp;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
const directionDetails = summarizeAiTrainEvaluationsByDirection(rows).filter(({ direction }) => direction === "LONG" || direction === "SHORT").flatMap(({ direction, summary: directionSummary }) => [
|
|
2024
|
+
{
|
|
2025
|
+
id: `direction:${direction}:approved`,
|
|
2026
|
+
label: `${direction} approved`,
|
|
2027
|
+
value: String(directionSummary.approved)
|
|
2028
|
+
},
|
|
2029
|
+
{
|
|
2030
|
+
id: `direction:${direction}:precision`,
|
|
2031
|
+
label: `${direction} precision`,
|
|
2032
|
+
value: formatRatio(directionSummary.precisionApproved)
|
|
2033
|
+
},
|
|
2034
|
+
{
|
|
2035
|
+
id: `direction:${direction}:monthlyPnl`,
|
|
2036
|
+
label: `${direction} monthly_pnl`,
|
|
2037
|
+
value: formatSigned(directionSummary.avgProfitApprovedPerMonth),
|
|
2038
|
+
tone: resolveMetricTone(directionSummary.avgProfitApprovedPerMonth)
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
id: `direction:${direction}:pnl`,
|
|
2042
|
+
label: `${direction} pnl`,
|
|
2043
|
+
value: formatSigned(directionSummary.approvedRisk.totalProfit),
|
|
2044
|
+
tone: resolveMetricTone(directionSummary.approvedRisk.totalProfit)
|
|
2045
|
+
},
|
|
2046
|
+
{
|
|
2047
|
+
id: `direction:${direction}:avgProfit`,
|
|
2048
|
+
label: `${direction} avg_profit`,
|
|
2049
|
+
value: formatSigned(directionSummary.avgProfitApproved),
|
|
2050
|
+
tone: resolveMetricTone(directionSummary.avgProfitApproved)
|
|
2051
|
+
}
|
|
2052
|
+
]);
|
|
2053
|
+
const symbolDetails = [
|
|
2054
|
+
...rows.reduce((grouped, row) => {
|
|
2055
|
+
if (!row.symbol) {
|
|
2056
|
+
return grouped;
|
|
2057
|
+
}
|
|
2058
|
+
const symbolRows = grouped.get(row.symbol) ?? [];
|
|
2059
|
+
symbolRows.push(row);
|
|
2060
|
+
grouped.set(row.symbol, symbolRows);
|
|
2061
|
+
return grouped;
|
|
2062
|
+
}, /* @__PURE__ */ new Map()).entries()
|
|
2063
|
+
].map(([symbol, symbolRows]) => {
|
|
2064
|
+
const symbolSummary = summarizeAiTrainEvaluations(symbolRows);
|
|
2065
|
+
return {
|
|
2066
|
+
symbol,
|
|
2067
|
+
summary: symbolSummary,
|
|
2068
|
+
pnl: symbolSummary.approvedRisk.totalProfit
|
|
2069
|
+
};
|
|
2070
|
+
}).filter(({ summary: summary2 }) => summary2.approved > 0).sort(
|
|
2071
|
+
(left, right) => Math.abs(right.pnl) - Math.abs(left.pnl) || right.pnl - left.pnl || left.symbol.localeCompare(right.symbol)
|
|
2072
|
+
).slice(0, 10).flatMap(({ symbol, summary: summary2, pnl }) => [
|
|
2073
|
+
{
|
|
2074
|
+
id: `symbol:${symbol}:pnl`,
|
|
2075
|
+
label: `${symbol} pnl`,
|
|
2076
|
+
value: formatSigned(pnl),
|
|
2077
|
+
tone: resolveMetricTone(pnl)
|
|
2078
|
+
},
|
|
2079
|
+
{
|
|
2080
|
+
id: `symbol:${symbol}:orders`,
|
|
2081
|
+
label: `${symbol} approved`,
|
|
2082
|
+
value: String(summary2.approved)
|
|
2083
|
+
},
|
|
2084
|
+
{
|
|
2085
|
+
id: `symbol:${symbol}:winRate`,
|
|
2086
|
+
label: `${symbol} win_rate`,
|
|
2087
|
+
value: formatRatio(summary2.approvedRisk.winRate)
|
|
2088
|
+
}
|
|
2089
|
+
]);
|
|
2090
|
+
const details = [
|
|
2091
|
+
{
|
|
2092
|
+
id: "window",
|
|
2093
|
+
label: "window",
|
|
2094
|
+
value: `${formatUtcWindowTimestamp(windowStart)} -> ${formatUtcWindowTimestamp(windowEnd)}`
|
|
2095
|
+
},
|
|
2096
|
+
{
|
|
2097
|
+
id: "approved",
|
|
2098
|
+
label: "approved",
|
|
2099
|
+
value: String(summary.approved)
|
|
2100
|
+
},
|
|
2101
|
+
{
|
|
2102
|
+
id: "confusion",
|
|
2103
|
+
label: "TP / FP / TN / FN",
|
|
2104
|
+
value: `${summary.truePositive} / ${summary.falsePositive} / ${summary.trueNegative} / ${summary.falseNegative}`
|
|
2105
|
+
},
|
|
2106
|
+
{
|
|
2107
|
+
id: "precisionApproved",
|
|
2108
|
+
label: "precision_approved",
|
|
2109
|
+
value: formatRatio(summary.precisionApproved)
|
|
2110
|
+
},
|
|
2111
|
+
{
|
|
2112
|
+
id: "recallWinners",
|
|
2113
|
+
label: "recall_winners",
|
|
2114
|
+
value: formatRatio(summary.recallWinners)
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
id: "avgProfitAll",
|
|
2118
|
+
label: "avg_profit_all",
|
|
2119
|
+
value: formatSigned(summary.avgProfitAll),
|
|
2120
|
+
tone: resolveMetricTone(summary.avgProfitAll)
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
id: "avgProfitApproved",
|
|
2124
|
+
label: "avg_profit_approved",
|
|
2125
|
+
value: formatSigned(summary.avgProfitApproved),
|
|
2126
|
+
tone: resolveMetricTone(summary.avgProfitApproved)
|
|
2127
|
+
},
|
|
2128
|
+
{
|
|
2129
|
+
id: "avgProfitApprovedPerDay",
|
|
2130
|
+
label: "avg_profit_approved_per_day",
|
|
2131
|
+
value: formatSigned(summary.avgProfitApprovedPerDay),
|
|
2132
|
+
tone: resolveMetricTone(summary.avgProfitApprovedPerDay)
|
|
2133
|
+
},
|
|
2134
|
+
{
|
|
2135
|
+
id: "avgProfitApprovedPerMonth",
|
|
2136
|
+
label: "avg_profit_approved_per_month",
|
|
2137
|
+
value: formatSigned(summary.avgProfitApprovedPerMonth),
|
|
2138
|
+
tone: resolveMetricTone(summary.avgProfitApprovedPerMonth)
|
|
2139
|
+
},
|
|
2140
|
+
{
|
|
2141
|
+
id: "avgApprovedTradesPerDay",
|
|
2142
|
+
label: "avg_approved_trades_per_day",
|
|
2143
|
+
value: formatNumber(summary.avgApprovedTradesPerDay)
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
id: "avgApprovedTradesPerWeek",
|
|
2147
|
+
label: "avg_approved_trades_per_week",
|
|
2148
|
+
value: formatNumber(summary.avgApprovedTradesPerWeek)
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
id: "expectancyDelta",
|
|
2152
|
+
label: "expectancy_delta",
|
|
2153
|
+
value: formatSigned(summary.expectancyDelta),
|
|
2154
|
+
tone: resolveMetricTone(summary.expectancyDelta)
|
|
2155
|
+
},
|
|
2156
|
+
{
|
|
2157
|
+
id: "maxLossStreak",
|
|
2158
|
+
label: "max_loss_streak",
|
|
2159
|
+
value: String(summary.approvedRisk.maxConsecutiveLosses),
|
|
2160
|
+
tone: summary.approvedRisk.maxConsecutiveLosses > 0 ? "warning" : "success"
|
|
2161
|
+
},
|
|
2162
|
+
...directionDetails,
|
|
2163
|
+
...symbolDetails
|
|
2164
|
+
];
|
|
2165
|
+
return details;
|
|
2166
|
+
};
|
|
2167
|
+
var buildStrategyWideEquityCurve = (evaluations) => {
|
|
2168
|
+
if (!evaluations.length) {
|
|
2169
|
+
return [];
|
|
2170
|
+
}
|
|
2171
|
+
const sorted = [...evaluations].sort(
|
|
2172
|
+
(left, right) => (left.timestamp ?? 0) - (right.timestamp ?? 0) || left.symbol.localeCompare(right.symbol) || left.signalId.localeCompare(right.signalId)
|
|
2173
|
+
);
|
|
2174
|
+
let amount = 100;
|
|
2175
|
+
const firstTimestamp = sorted[0]?.timestamp ?? Date.now();
|
|
2176
|
+
const orderLog = [[firstTimestamp, amount]];
|
|
2177
|
+
for (const evaluation of sorted) {
|
|
2178
|
+
amount += evaluation.profit;
|
|
2179
|
+
orderLog.push([
|
|
2180
|
+
evaluation.timestamp ?? firstTimestamp,
|
|
2181
|
+
Number(amount.toFixed(4))
|
|
2182
|
+
]);
|
|
2183
|
+
}
|
|
2184
|
+
if (orderLog.length === 1) {
|
|
2185
|
+
orderLog.push([firstTimestamp, amount]);
|
|
2186
|
+
}
|
|
2187
|
+
return orderLog;
|
|
2188
|
+
};
|
|
2189
|
+
var sortAiChartEvaluations = (evaluations) => [...evaluations].sort(
|
|
2190
|
+
(left, right) => (left.timestamp ?? 0) - (right.timestamp ?? 0) || left.symbol.localeCompare(right.symbol) || left.signalId.localeCompare(right.signalId)
|
|
2191
|
+
);
|
|
2192
|
+
var toFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2193
|
+
var buildStrategyWideOrders = (evaluations) => {
|
|
2194
|
+
let amount = 100;
|
|
2195
|
+
return sortAiChartEvaluations(evaluations).flatMap((evaluation, index) => {
|
|
2196
|
+
const tradeResult = evaluation.tradeResult;
|
|
2197
|
+
const equityBefore = amount;
|
|
2198
|
+
amount = Number((amount + evaluation.profit).toFixed(4));
|
|
2199
|
+
if (!tradeResult) {
|
|
2200
|
+
return [];
|
|
2201
|
+
}
|
|
2202
|
+
const entryPrice = toFiniteNumber(tradeResult?.entryPrice);
|
|
2203
|
+
const qty = toFiniteNumber(tradeResult?.qty);
|
|
2204
|
+
const notional = entryPrice != null && qty != null ? Number((entryPrice * qty).toFixed(8)) : null;
|
|
2205
|
+
return {
|
|
2206
|
+
id: `${evaluation.signalId}:${evaluation.timestamp ?? index}`,
|
|
2207
|
+
symbol: evaluation.symbol,
|
|
2208
|
+
direction: tradeResult?.direction ?? evaluation.direction ?? null,
|
|
2209
|
+
timestamp: evaluation.timestamp ?? tradeResult?.exitTimestamp ?? null,
|
|
2210
|
+
entryTimestamp: tradeResult?.entryTimestamp ?? evaluation.timestamp ?? null,
|
|
2211
|
+
exitTimestamp: tradeResult?.exitTimestamp ?? evaluation.timestamp ?? null,
|
|
2212
|
+
exitReason: tradeResult?.exitReason ?? null,
|
|
2213
|
+
pnl: evaluation.profit,
|
|
2214
|
+
equityBefore,
|
|
2215
|
+
equityAfter: amount,
|
|
2216
|
+
qty,
|
|
2217
|
+
notional,
|
|
2218
|
+
requestedEntryPrice: toFiniteNumber(tradeResult?.requestedEntryPrice),
|
|
2219
|
+
entryPrice,
|
|
2220
|
+
requestedExitPrice: toFiniteNumber(tradeResult?.requestedExitPrice),
|
|
2221
|
+
exitPrice: toFiniteNumber(tradeResult?.exitPrice),
|
|
2222
|
+
openFee: toFiniteNumber(tradeResult?.openFee),
|
|
2223
|
+
closeFee: toFiniteNumber(tradeResult?.closeFee),
|
|
2224
|
+
fundingFee: toFiniteNumber(tradeResult?.fundingFee),
|
|
2225
|
+
totalFee: toFiniteNumber(tradeResult?.totalFee),
|
|
2226
|
+
entrySlippageBps: toFiniteNumber(tradeResult?.entrySlippageBps),
|
|
2227
|
+
entryBaseSlippageBps: toFiniteNumber(tradeResult?.entryBaseSlippageBps),
|
|
2228
|
+
entrySpreadBps: toFiniteNumber(tradeResult?.entrySpreadBps),
|
|
2229
|
+
entrySpreadSlippageBps: toFiniteNumber(
|
|
2230
|
+
tradeResult?.entrySpreadSlippageBps
|
|
2231
|
+
),
|
|
2232
|
+
entryMarketImpactBps: toFiniteNumber(tradeResult?.entryMarketImpactBps),
|
|
2233
|
+
entryDelayRiskBps: toFiniteNumber(tradeResult?.entryDelayRiskBps),
|
|
2234
|
+
exitSlippageBps: toFiniteNumber(tradeResult?.exitSlippageBps),
|
|
2235
|
+
exitBaseSlippageBps: toFiniteNumber(tradeResult?.exitBaseSlippageBps),
|
|
2236
|
+
exitSpreadBps: toFiniteNumber(tradeResult?.exitSpreadBps),
|
|
2237
|
+
exitSpreadSlippageBps: toFiniteNumber(tradeResult?.exitSpreadSlippageBps),
|
|
2238
|
+
exitMarketImpactBps: toFiniteNumber(tradeResult?.exitMarketImpactBps),
|
|
2239
|
+
exitDelayRiskBps: toFiniteNumber(tradeResult?.exitDelayRiskBps),
|
|
2240
|
+
totalSlippageCost: toFiniteNumber(tradeResult?.totalSlippageCost),
|
|
2241
|
+
sequence: evaluation.sequence ?? index + 1
|
|
2242
|
+
};
|
|
2243
|
+
});
|
|
2244
|
+
};
|
|
2245
|
+
var calculateMaxDrawdownPercent = (orderLog) => {
|
|
2246
|
+
if (!orderLog.length) {
|
|
2247
|
+
return null;
|
|
2248
|
+
}
|
|
2249
|
+
let peak = orderLog[0]?.[1] ?? 0;
|
|
2250
|
+
let maxDrawdownPercent = 0;
|
|
2251
|
+
for (const [, amount] of orderLog) {
|
|
2252
|
+
if (!Number.isFinite(amount)) {
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
peak = Math.max(peak, amount);
|
|
2256
|
+
if (peak <= 0) {
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
const drawdownPercent = (peak - amount) / peak * 100;
|
|
2260
|
+
maxDrawdownPercent = Math.max(maxDrawdownPercent, drawdownPercent);
|
|
2261
|
+
}
|
|
2262
|
+
return Number(maxDrawdownPercent.toFixed(1));
|
|
2263
|
+
};
|
|
2264
|
+
var resolveAiVariantGroup = (params) => {
|
|
2265
|
+
const normalizedConfigId = params.configId.trim();
|
|
2266
|
+
if (!normalizedConfigId) {
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
return {
|
|
2270
|
+
key: normalizedConfigId,
|
|
2271
|
+
label: `config ${normalizedConfigId}`
|
|
2272
|
+
};
|
|
2273
|
+
};
|
|
2274
|
+
var buildAiChartSnapshot = (params) => {
|
|
2275
|
+
const {
|
|
2276
|
+
evaluatedRows,
|
|
2277
|
+
strategyName,
|
|
2278
|
+
generatedAt,
|
|
2279
|
+
runLabel,
|
|
2280
|
+
minQuality,
|
|
2281
|
+
datasetId
|
|
2282
|
+
} = params;
|
|
2283
|
+
const groupsByVariant = /* @__PURE__ */ new Map();
|
|
2284
|
+
for (const evaluation of evaluatedRows) {
|
|
2285
|
+
const variantGroup = resolveAiVariantGroup({
|
|
2286
|
+
configId: evaluation.configId
|
|
2287
|
+
});
|
|
2288
|
+
if (!variantGroup) {
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
const existingGroup = groupsByVariant.get(variantGroup.key);
|
|
2292
|
+
if (existingGroup) {
|
|
2293
|
+
existingGroup.rows.push(evaluation);
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
groupsByVariant.set(variantGroup.key, {
|
|
2297
|
+
key: variantGroup.key,
|
|
2298
|
+
label: variantGroup.label,
|
|
2299
|
+
rows: [evaluation]
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
const variantGroups = [...groupsByVariant.values()].sort(
|
|
2303
|
+
(left, right) => left.label.localeCompare(right.label)
|
|
2304
|
+
);
|
|
2305
|
+
const groupByVariant = variantGroups.length > 1;
|
|
2306
|
+
const groups = groupByVariant ? variantGroups : [
|
|
2307
|
+
{
|
|
2308
|
+
key: strategyName,
|
|
2309
|
+
label: "",
|
|
2310
|
+
rows: evaluatedRows
|
|
2311
|
+
}
|
|
2312
|
+
];
|
|
2313
|
+
const cards = [];
|
|
2314
|
+
for (const threshold of [minQuality]) {
|
|
2315
|
+
for (const group of groups) {
|
|
2316
|
+
const thresholdEvaluations = group.rows.map((evaluation) => ({
|
|
2317
|
+
...evaluation,
|
|
2318
|
+
aiApproved: evaluation.modelDirection === evaluation.direction && evaluation.quality != null && evaluation.quality >= threshold
|
|
2319
|
+
}));
|
|
2320
|
+
const summary = summarizeAiTrainEvaluations(thresholdEvaluations);
|
|
2321
|
+
const approvedRows = thresholdEvaluations.filter(
|
|
2322
|
+
(evaluation) => evaluation.aiApproved
|
|
2323
|
+
);
|
|
2324
|
+
const orderLog = buildStrategyWideEquityCurve(approvedRows);
|
|
2325
|
+
const orders = buildStrategyWideOrders(approvedRows);
|
|
2326
|
+
cards.push({
|
|
2327
|
+
cardId: `${strategyName}-${group.key}-q${threshold}-${generatedAt}`,
|
|
2328
|
+
generatedAt,
|
|
2329
|
+
strategyName,
|
|
2330
|
+
title: groupByVariant ? `${strategyName} \xB7 ${group.label}` : strategyName,
|
|
2331
|
+
subtitle: runLabel ? `q${threshold}+ \xB7 ${runLabel}` : `q${threshold}+`,
|
|
2332
|
+
datasetId,
|
|
2333
|
+
symbols: [
|
|
2334
|
+
...new Set(
|
|
2335
|
+
group.rows.map((evaluation) => evaluation.symbol).filter(Boolean)
|
|
2336
|
+
)
|
|
2337
|
+
].sort(),
|
|
2338
|
+
orderLog,
|
|
2339
|
+
orders,
|
|
2340
|
+
stat: null,
|
|
2341
|
+
metrics: buildAiChartMetrics({
|
|
2342
|
+
summary,
|
|
2343
|
+
maxDrawdownPercent: calculateMaxDrawdownPercent(orderLog)
|
|
2344
|
+
}),
|
|
2345
|
+
details: buildAiChartDetails({
|
|
2346
|
+
summary,
|
|
2347
|
+
rows: thresholdEvaluations
|
|
2348
|
+
}),
|
|
2349
|
+
tags: groupByVariant ? [`q${threshold}+`, group.label] : [`q${threshold}+`]
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
return {
|
|
2354
|
+
mode: "ai",
|
|
2355
|
+
generatedAt,
|
|
2356
|
+
runLabel,
|
|
2357
|
+
strategies: cards
|
|
2358
|
+
};
|
|
2359
|
+
};
|
|
2360
|
+
|
|
2361
|
+
// src/lib/aiTrainEvaluationDump.ts
|
|
2362
|
+
var BASE_CONTEXT_FEATURE_SECTIONS = [
|
|
2363
|
+
"regime",
|
|
2364
|
+
"structure",
|
|
2365
|
+
"participation",
|
|
2366
|
+
"relative",
|
|
2367
|
+
"derivatives",
|
|
2368
|
+
"mtf",
|
|
2369
|
+
"gateFeatures"
|
|
2370
|
+
];
|
|
2371
|
+
var asRecord = (value) => typeof value === "object" && value != null && !Array.isArray(value) ? value : null;
|
|
2372
|
+
var collectStrategyGateFeatures = (baseContext) => {
|
|
2373
|
+
const entries = Object.entries(baseContext).filter(
|
|
2374
|
+
([key, value]) => key !== "gateFeatures" && key.endsWith("GateFeatures") && asRecord(value) != null
|
|
2375
|
+
);
|
|
2376
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
2377
|
+
};
|
|
2378
|
+
var pickBaseContextFeatureSections = (baseContext) => Object.fromEntries(
|
|
2379
|
+
BASE_CONTEXT_FEATURE_SECTIONS.map((section) => [
|
|
2380
|
+
section,
|
|
2381
|
+
baseContext[section] ?? null
|
|
2382
|
+
])
|
|
2383
|
+
);
|
|
2384
|
+
var buildAiTrainEvaluationFeatureSnapshot = ({
|
|
2385
|
+
additionalIndicators,
|
|
2386
|
+
mode
|
|
2387
|
+
}) => {
|
|
2388
|
+
if (mode === "none") {
|
|
2389
|
+
return void 0;
|
|
2390
|
+
}
|
|
2391
|
+
const additional = asRecord(additionalIndicators);
|
|
2392
|
+
const baseContext = asRecord(additional?.baseContext);
|
|
2393
|
+
if (!baseContext) {
|
|
2394
|
+
return {
|
|
2395
|
+
baseContextAvailable: false
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
const strategyGateFeatures = collectStrategyGateFeatures(baseContext);
|
|
2399
|
+
const snapshot = {
|
|
2400
|
+
baseContextAvailable: true,
|
|
2401
|
+
gateFeatures: baseContext.gateFeatures ?? null,
|
|
2402
|
+
...strategyGateFeatures ? { strategyGateFeatures } : {}
|
|
2403
|
+
};
|
|
2404
|
+
if (mode === "baseContext") {
|
|
2405
|
+
snapshot.baseContext = pickBaseContextFeatureSections(baseContext);
|
|
2406
|
+
}
|
|
2407
|
+
return snapshot;
|
|
2408
|
+
};
|
|
2409
|
+
|
|
2410
|
+
// src/lib/aiTrainOptions.ts
|
|
2411
|
+
var parseTimestampFilter = (value) => {
|
|
2412
|
+
if (value == null || String(value).trim() === "") {
|
|
2413
|
+
return null;
|
|
2414
|
+
}
|
|
2415
|
+
const text = String(value).trim();
|
|
2416
|
+
const numeric = Number(text);
|
|
2417
|
+
if (Number.isFinite(numeric)) {
|
|
2418
|
+
return numeric;
|
|
2419
|
+
}
|
|
2420
|
+
const parsed = Date.parse(text);
|
|
2421
|
+
if (Number.isFinite(parsed)) {
|
|
2422
|
+
return parsed;
|
|
2423
|
+
}
|
|
2424
|
+
throw new Error(
|
|
2425
|
+
`Invalid timestamp filter "${text}". Use an ISO date or epoch milliseconds.`
|
|
2426
|
+
);
|
|
2427
|
+
};
|
|
2428
|
+
var parseTrailingPeriodMs = (value) => {
|
|
2429
|
+
if (value == null || String(value).trim() === "") {
|
|
2430
|
+
return null;
|
|
2431
|
+
}
|
|
2432
|
+
const text = String(value).trim().toLowerCase();
|
|
2433
|
+
const match = text.match(
|
|
2434
|
+
/^(?:last)?(\d+)(d|day|days|w|week|weeks|m|month|months|y|year|years)$/
|
|
2435
|
+
);
|
|
2436
|
+
if (!match) {
|
|
2437
|
+
throw new Error(
|
|
2438
|
+
`Invalid --period value "${String(value)}". Use values like last365d, 90d, 12w, or 1y.`
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
const amount = Number(match[1]);
|
|
2442
|
+
const unit = match[2];
|
|
2443
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
2444
|
+
if (unit.startsWith("w")) {
|
|
2445
|
+
return amount * 7 * dayMs;
|
|
2446
|
+
}
|
|
2447
|
+
if (unit.startsWith("m")) {
|
|
2448
|
+
return amount * 30.4375 * dayMs;
|
|
2449
|
+
}
|
|
2450
|
+
if (unit.startsWith("y")) {
|
|
2451
|
+
return amount * 365 * dayMs;
|
|
2452
|
+
}
|
|
2453
|
+
return amount * dayMs;
|
|
2454
|
+
};
|
|
2455
|
+
var parseQualityThresholds = (value) => {
|
|
2456
|
+
const raw = String(value ?? "3,4,5").split(",").map((part) => Number(part.trim())).filter((part) => Number.isFinite(part)).map((part) => Math.trunc(part)).filter((part) => part > 0);
|
|
2457
|
+
return [...new Set(raw)].sort((left, right) => left - right);
|
|
2458
|
+
};
|
|
2459
|
+
var parseTerminalWindowDays = (value) => {
|
|
2460
|
+
const raw = String(value ?? "90,30,7").split(",").map((part) => Number(part.trim())).filter((part) => Number.isFinite(part)).map((part) => Math.trunc(part)).filter((part) => part > 0);
|
|
2461
|
+
return [...new Set(raw)].sort((left, right) => right - left);
|
|
2462
|
+
};
|
|
2463
|
+
var parseDumpFeatureMode = (value) => {
|
|
2464
|
+
const text = String(value ?? "none").trim().toLowerCase();
|
|
2465
|
+
if (!text || text === "none" || text === "false" || text === "0") {
|
|
2466
|
+
return "none";
|
|
2467
|
+
}
|
|
2468
|
+
if (text === "gatefeatures" || text === "gate-features") {
|
|
2469
|
+
return "gateFeatures";
|
|
2470
|
+
}
|
|
2471
|
+
if (text === "basecontext" || text === "base-context") {
|
|
2472
|
+
return "baseContext";
|
|
2473
|
+
}
|
|
2474
|
+
throw new Error(
|
|
2475
|
+
`Invalid --dumpFeatures value "${String(value)}". Use none, gateFeatures, or baseContext.`
|
|
2476
|
+
);
|
|
2477
|
+
};
|
|
2478
|
+
var hasCliOption = ({
|
|
2479
|
+
argv,
|
|
2480
|
+
longName,
|
|
2481
|
+
shortName
|
|
2482
|
+
}) => argv.some(
|
|
2483
|
+
(arg) => arg === `--${longName}` || arg.startsWith(`--${longName}=`) || arg === `-${shortName}` || arg.startsWith(`-${shortName}=`)
|
|
2484
|
+
);
|
|
2485
|
+
var resolveAiTrainRecentLimit = ({
|
|
2486
|
+
argv,
|
|
2487
|
+
recentValue,
|
|
2488
|
+
hasDateFilter,
|
|
2489
|
+
defaultRecent = 50
|
|
2490
|
+
}) => {
|
|
2491
|
+
const hasExplicitRecent = hasCliOption({
|
|
2492
|
+
argv,
|
|
2493
|
+
longName: "recent",
|
|
2494
|
+
shortName: "n"
|
|
2495
|
+
});
|
|
2496
|
+
const selectedRecent = hasDateFilter && !hasExplicitRecent ? 0 : recentValue;
|
|
2497
|
+
const parsed = Number(selectedRecent);
|
|
2498
|
+
return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : defaultRecent;
|
|
2499
|
+
};
|
|
2500
|
+
|
|
2501
|
+
// src/lib/aiTrainResearch.ts
|
|
2502
|
+
var import_node_child_process = require("child_process");
|
|
2503
|
+
var import_node_crypto = require("crypto");
|
|
2504
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
2505
|
+
var import_node_path = __toESM(require("path"));
|
|
2506
|
+
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
2507
|
+
var RESEARCH_CONTEXT_ENV_KEYS = [
|
|
2508
|
+
"AI_MODE",
|
|
2509
|
+
"MIN_AI_QUALITY",
|
|
2510
|
+
"INTERVAL",
|
|
2511
|
+
"DERIVATIVES_CONTEXT_ENABLED",
|
|
2512
|
+
"DERIVATIVES_CONTEXT_TARGET_ENABLED",
|
|
2513
|
+
"DERIVATIVES_CONTEXT_INTERVALS",
|
|
2514
|
+
"DERIVATIVES_CONTEXT_LOOKBACK_HOURS",
|
|
2515
|
+
"DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS",
|
|
2516
|
+
"DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY",
|
|
2517
|
+
"COINMARKETCAP_CONTEXT_ENABLED",
|
|
2518
|
+
"COINMARKETCAP_CONTEXT_MAX_AGE_MS",
|
|
2519
|
+
"COINMARKETCAP_CONTEXT_EXCHANGE_LIQUIDITY_ENABLED",
|
|
2520
|
+
"COINMARKETCAP_CONTEXT_EXCHANGE_SLUGS",
|
|
2521
|
+
"COINMARKETCAP_CONTEXT_FEAR_GREED_ENABLED",
|
|
2522
|
+
"COINMARKETCAP_CONTEXT_HISTORICAL_ACCESS_MONTHS",
|
|
2523
|
+
"COINMARKETCAP_CONTEXT_BACKFILL_ENABLED",
|
|
2524
|
+
"COINMARKETCAP_CONTEXT_BACKFILL_MAX_DAYS",
|
|
2525
|
+
"COINMARKETCAP_CONTEXT_BACKFILL_WARMUP_DAYS"
|
|
2526
|
+
];
|
|
2527
|
+
var normalizeForStableJson = (value) => {
|
|
2528
|
+
if (Array.isArray(value)) {
|
|
2529
|
+
return value.map(normalizeForStableJson);
|
|
2530
|
+
}
|
|
2531
|
+
if (value && typeof value === "object") {
|
|
2532
|
+
return Object.fromEntries(
|
|
2533
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeForStableJson(entry)])
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
return value;
|
|
2537
|
+
};
|
|
2538
|
+
var fingerprintResearchValue = (value) => (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
|
|
2539
|
+
var getTimestampRange = (evaluations) => {
|
|
2540
|
+
let minTimestamp = null;
|
|
2541
|
+
let maxTimestamp = null;
|
|
2542
|
+
for (const evaluation of evaluations) {
|
|
2543
|
+
const timestamp = evaluation.timestamp;
|
|
2544
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
minTimestamp = minTimestamp == null ? timestamp : Math.min(minTimestamp, timestamp);
|
|
2548
|
+
maxTimestamp = maxTimestamp == null ? timestamp : Math.max(maxTimestamp, timestamp);
|
|
2549
|
+
}
|
|
2550
|
+
return { minTimestamp, maxTimestamp };
|
|
2551
|
+
};
|
|
2552
|
+
var summarizeAiTrainCoverage = (evaluations, now = Date.now()) => {
|
|
2553
|
+
const { minTimestamp, maxTimestamp } = getTimestampRange(evaluations);
|
|
2554
|
+
const timestamped = evaluations.filter(
|
|
2555
|
+
(evaluation) => typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp)
|
|
2556
|
+
).length;
|
|
2557
|
+
return {
|
|
2558
|
+
timestamped,
|
|
2559
|
+
missingTimestamps: evaluations.length - timestamped,
|
|
2560
|
+
minTimestamp,
|
|
2561
|
+
maxTimestamp,
|
|
2562
|
+
spanDays: minTimestamp == null || maxTimestamp == null ? null : (maxTimestamp - minTimestamp) / DAY_MS2,
|
|
2563
|
+
dataLagDays: maxTimestamp == null ? null : Math.max(0, (now - maxTimestamp) / DAY_MS2)
|
|
2564
|
+
};
|
|
2565
|
+
};
|
|
2566
|
+
var summarizeAiTrainRejectReasons = (evaluations, limit = 5) => {
|
|
2567
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2568
|
+
for (const evaluation of evaluations) {
|
|
2569
|
+
if (evaluation.aiApproved || !evaluation.rejectReason) {
|
|
2570
|
+
continue;
|
|
2571
|
+
}
|
|
2572
|
+
for (const reason of evaluation.rejectReason.split(";").map((part) => part.trim()).filter(Boolean)) {
|
|
2573
|
+
counts.set(reason, (counts.get(reason) ?? 0) + 1);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
return [...counts.entries()].sort(
|
|
2577
|
+
(left, right) => right[1] - left[1] || left[0].localeCompare(right[0])
|
|
2578
|
+
).slice(0, limit).map(([reason, count]) => ({ reason, count }));
|
|
2579
|
+
};
|
|
2580
|
+
var summarizeAiTrainTerminalWindows = (evaluations, windowDays) => {
|
|
2581
|
+
const { minTimestamp, maxTimestamp } = getTimestampRange(evaluations);
|
|
2582
|
+
if (minTimestamp == null || maxTimestamp == null) {
|
|
2583
|
+
return [];
|
|
2584
|
+
}
|
|
2585
|
+
return windowDays.map((days) => {
|
|
2586
|
+
const requestedSince = maxTimestamp - days * DAY_MS2;
|
|
2587
|
+
const since = Math.max(minTimestamp, requestedSince);
|
|
2588
|
+
const selected = evaluations.filter(
|
|
2589
|
+
(evaluation) => typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp) && evaluation.timestamp >= requestedSince && evaluation.timestamp <= maxTimestamp
|
|
2590
|
+
);
|
|
2591
|
+
const coverageDays = Math.max(1, (maxTimestamp - since) / DAY_MS2);
|
|
2592
|
+
const outcome = summarizeAiTrainEvaluations(selected);
|
|
2593
|
+
return {
|
|
2594
|
+
label: `last${days}d`,
|
|
2595
|
+
days,
|
|
2596
|
+
complete: minTimestamp <= requestedSince,
|
|
2597
|
+
since,
|
|
2598
|
+
until: maxTimestamp,
|
|
2599
|
+
coverageDays,
|
|
2600
|
+
selected: selected.length,
|
|
2601
|
+
approvedPerCalendarDay: outcome.approved / coverageDays,
|
|
2602
|
+
outcome,
|
|
2603
|
+
topRejectReasons: summarizeAiTrainRejectReasons(selected)
|
|
2604
|
+
};
|
|
2605
|
+
});
|
|
2606
|
+
};
|
|
2607
|
+
var readOptionalFile = async (filePath) => {
|
|
2608
|
+
try {
|
|
2609
|
+
return await import_promises.default.readFile(filePath);
|
|
2610
|
+
} catch {
|
|
2611
|
+
return null;
|
|
2612
|
+
}
|
|
2613
|
+
};
|
|
2614
|
+
var getGitLineage = (projectRoot) => {
|
|
2615
|
+
try {
|
|
2616
|
+
const gitSha = (0, import_node_child_process.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
2617
|
+
cwd: projectRoot,
|
|
2618
|
+
encoding: "utf8",
|
|
2619
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2620
|
+
}).trim();
|
|
2621
|
+
const status = (0, import_node_child_process.execFileSync)(
|
|
2622
|
+
"git",
|
|
2623
|
+
["status", "--porcelain", "--untracked-files=no"],
|
|
2624
|
+
{
|
|
2625
|
+
cwd: projectRoot,
|
|
2626
|
+
encoding: "utf8",
|
|
2627
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2628
|
+
}
|
|
2629
|
+
).trim();
|
|
2630
|
+
return { gitSha: gitSha || null, gitDirty: status.length > 0 };
|
|
2631
|
+
} catch {
|
|
2632
|
+
return { gitSha: null, gitDirty: null };
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
var resolveGateFingerprint = async (projectRoot, strategyName, gitSha) => {
|
|
2636
|
+
const relativeCandidates = [
|
|
2637
|
+
`packages/strategies/src/${strategyName}/adapters/ai.ts`,
|
|
2638
|
+
`packages/strategies/src/${strategyName}/guardrails.ts`,
|
|
2639
|
+
`packages/strategies/src/${strategyName}/config.ts`,
|
|
2640
|
+
"packages/node/src/ai.ts"
|
|
2641
|
+
];
|
|
2642
|
+
const optionalSourceEntries = await Promise.all(
|
|
2643
|
+
relativeCandidates.map(async (relativePath) => {
|
|
2644
|
+
const content = await readOptionalFile(
|
|
2645
|
+
import_node_path.default.join(projectRoot, relativePath)
|
|
2646
|
+
);
|
|
2647
|
+
return content == null ? null : { relativePath, content };
|
|
2648
|
+
})
|
|
2649
|
+
);
|
|
2650
|
+
const sourceEntries = [];
|
|
2651
|
+
for (const entry of optionalSourceEntries) {
|
|
2652
|
+
if (entry != null) {
|
|
2653
|
+
sourceEntries.push(entry);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
const hash = (0, import_node_crypto.createHash)("sha256");
|
|
2657
|
+
hash.update(strategyName);
|
|
2658
|
+
if (sourceEntries.length) {
|
|
2659
|
+
for (const entry of sourceEntries) {
|
|
2660
|
+
hash.update(entry.relativePath);
|
|
2661
|
+
hash.update(entry.content);
|
|
2662
|
+
}
|
|
2663
|
+
} else {
|
|
2664
|
+
hash.update(gitSha ?? "unknown-git-sha");
|
|
2665
|
+
}
|
|
2666
|
+
return {
|
|
2667
|
+
gateFingerprint: hash.digest("hex").slice(0, 16),
|
|
2668
|
+
gateFingerprintFiles: sourceEntries.map((entry) => entry.relativePath)
|
|
2669
|
+
};
|
|
2670
|
+
};
|
|
2671
|
+
var buildAiTrainLineage = async ({
|
|
2672
|
+
projectRoot,
|
|
2673
|
+
strategyName,
|
|
2674
|
+
configIds,
|
|
2675
|
+
runContext,
|
|
2676
|
+
env = process.env
|
|
2677
|
+
}) => {
|
|
2678
|
+
const { gitSha, gitDirty } = getGitLineage(projectRoot);
|
|
2679
|
+
const gate = await resolveGateFingerprint(projectRoot, strategyName, gitSha);
|
|
2680
|
+
const normalizedConfigIds = [...new Set(configIds.filter(Boolean))].sort();
|
|
2681
|
+
const context = {
|
|
2682
|
+
...Object.fromEntries(
|
|
2683
|
+
RESEARCH_CONTEXT_ENV_KEYS.map((key) => [key, env[key] ?? null])
|
|
2684
|
+
),
|
|
2685
|
+
...runContext
|
|
2686
|
+
};
|
|
2687
|
+
return {
|
|
2688
|
+
gitSha,
|
|
2689
|
+
gitDirty,
|
|
2690
|
+
...gate,
|
|
2691
|
+
configIdsFingerprint: fingerprintResearchValue(normalizedConfigIds),
|
|
2692
|
+
configIds: normalizedConfigIds,
|
|
2693
|
+
contextFingerprint: fingerprintResearchValue(context),
|
|
2694
|
+
context
|
|
2695
|
+
};
|
|
2696
|
+
};
|
|
2697
|
+
|
|
2698
|
+
// src/lib/aiTrainQuarantine.ts
|
|
2699
|
+
var import_crypto = require("crypto");
|
|
2700
|
+
var DAY_MS3 = 24 * 60 * 60 * 1e3;
|
|
2701
|
+
var normalizeKeyPart = (value, fallback) => {
|
|
2702
|
+
if (typeof value !== "string") {
|
|
2703
|
+
return fallback;
|
|
2704
|
+
}
|
|
2705
|
+
const trimmed = value.trim();
|
|
2706
|
+
return trimmed || fallback;
|
|
2707
|
+
};
|
|
2708
|
+
var getProfitFactor = ({
|
|
2709
|
+
grossProfit,
|
|
2710
|
+
grossLoss
|
|
2711
|
+
}) => {
|
|
2712
|
+
if (grossLoss <= 0) {
|
|
2713
|
+
return grossProfit > 0 ? null : 0;
|
|
2714
|
+
}
|
|
2715
|
+
return grossProfit / grossLoss;
|
|
2716
|
+
};
|
|
2717
|
+
var createEmptyState = () => ({
|
|
2718
|
+
quarantineUntil: null,
|
|
2719
|
+
approvedLosses: 0,
|
|
2720
|
+
grossProfit: 0,
|
|
2721
|
+
grossLoss: 0
|
|
2722
|
+
});
|
|
2723
|
+
var applyAiTrainSymbolQuarantine = (evaluations, options) => {
|
|
2724
|
+
const summary = {
|
|
2725
|
+
enabled: options.enabled,
|
|
2726
|
+
minApprovedLosses: options.minApprovedLosses,
|
|
2727
|
+
minProfitFactor: options.minProfitFactor,
|
|
2728
|
+
cooldownDays: options.cooldownDays,
|
|
2729
|
+
blocked: 0,
|
|
2730
|
+
events: []
|
|
2731
|
+
};
|
|
2732
|
+
if (!options.enabled) {
|
|
2733
|
+
return {
|
|
2734
|
+
evaluations: evaluations.map((evaluation) => ({
|
|
2735
|
+
...evaluation,
|
|
2736
|
+
rawAiApproved: evaluation.aiApproved
|
|
2737
|
+
})),
|
|
2738
|
+
summary
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
const cooldownMs = Math.max(0, options.cooldownDays) * DAY_MS3;
|
|
2742
|
+
const minApprovedLosses = Math.max(1, Math.trunc(options.minApprovedLosses));
|
|
2743
|
+
const minProfitFactor = Math.max(0, options.minProfitFactor);
|
|
2744
|
+
const states = /* @__PURE__ */ new Map();
|
|
2745
|
+
const ordered = evaluations.map((evaluation, index) => ({ evaluation, index })).sort((left, right) => {
|
|
2746
|
+
const leftTimestamp = typeof left.evaluation.timestamp === "number" && Number.isFinite(left.evaluation.timestamp) ? left.evaluation.timestamp : Number.POSITIVE_INFINITY;
|
|
2747
|
+
const rightTimestamp = typeof right.evaluation.timestamp === "number" && Number.isFinite(right.evaluation.timestamp) ? right.evaluation.timestamp : Number.POSITIVE_INFINITY;
|
|
2748
|
+
return leftTimestamp - rightTimestamp || left.index - right.index;
|
|
2749
|
+
});
|
|
2750
|
+
const result = evaluations.map((evaluation) => ({
|
|
2751
|
+
...evaluation,
|
|
2752
|
+
rawAiApproved: evaluation.aiApproved
|
|
2753
|
+
}));
|
|
2754
|
+
for (const { evaluation, index } of ordered) {
|
|
2755
|
+
const timestamp = typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp) ? evaluation.timestamp : null;
|
|
2756
|
+
const strategy = normalizeKeyPart(evaluation.strategy, "unknown");
|
|
2757
|
+
const symbol = normalizeKeyPart(evaluation.symbol, "unknown");
|
|
2758
|
+
const key = `${strategy}:${symbol}`;
|
|
2759
|
+
const state = states.get(key) ?? createEmptyState();
|
|
2760
|
+
states.set(key, state);
|
|
2761
|
+
if (timestamp != null && state.quarantineUntil != null && timestamp >= state.quarantineUntil) {
|
|
2762
|
+
state.quarantineUntil = null;
|
|
2763
|
+
state.approvedLosses = 0;
|
|
2764
|
+
state.grossProfit = 0;
|
|
2765
|
+
state.grossLoss = 0;
|
|
2766
|
+
}
|
|
2767
|
+
if (evaluation.aiApproved && timestamp != null && state.quarantineUntil != null && timestamp < state.quarantineUntil) {
|
|
2768
|
+
result[index] = {
|
|
2769
|
+
...result[index],
|
|
2770
|
+
aiApproved: false
|
|
2771
|
+
};
|
|
2772
|
+
summary.blocked += 1;
|
|
2773
|
+
continue;
|
|
2774
|
+
}
|
|
2775
|
+
if (!evaluation.aiApproved) {
|
|
2776
|
+
continue;
|
|
2777
|
+
}
|
|
2778
|
+
if (evaluation.profit > 0) {
|
|
2779
|
+
state.grossProfit += evaluation.profit;
|
|
2780
|
+
} else if (evaluation.profit < 0) {
|
|
2781
|
+
state.grossLoss += Math.abs(evaluation.profit);
|
|
2782
|
+
state.approvedLosses += 1;
|
|
2783
|
+
}
|
|
2784
|
+
const profitFactor = getProfitFactor(state);
|
|
2785
|
+
if (timestamp != null && cooldownMs > 0 && state.approvedLosses >= minApprovedLosses && profitFactor != null && profitFactor < minProfitFactor) {
|
|
2786
|
+
state.quarantineUntil = timestamp + cooldownMs;
|
|
2787
|
+
summary.events.push({
|
|
2788
|
+
strategy,
|
|
2789
|
+
symbol,
|
|
2790
|
+
startedAt: timestamp,
|
|
2791
|
+
until: state.quarantineUntil,
|
|
2792
|
+
approvedLosses: state.approvedLosses,
|
|
2793
|
+
profitFactor,
|
|
2794
|
+
grossProfit: state.grossProfit,
|
|
2795
|
+
grossLoss: state.grossLoss
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
return {
|
|
2800
|
+
evaluations: result,
|
|
2801
|
+
summary
|
|
2802
|
+
};
|
|
2803
|
+
};
|
|
2804
|
+
var stableStringify = (value) => {
|
|
2805
|
+
if (value == null || typeof value !== "object") {
|
|
2806
|
+
return JSON.stringify(value);
|
|
2807
|
+
}
|
|
2808
|
+
if (Array.isArray(value)) {
|
|
2809
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
2810
|
+
}
|
|
2811
|
+
const record = value;
|
|
2812
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
2813
|
+
};
|
|
2814
|
+
var hashContext = (row) => {
|
|
2815
|
+
const payload = row.payload;
|
|
2816
|
+
const signal = payload?.signal;
|
|
2817
|
+
const additional = payload?.additionalIndicators && typeof payload.additionalIndicators === "object" && !Array.isArray(payload.additionalIndicators) ? payload.additionalIndicators : {};
|
|
2818
|
+
const context = {
|
|
2819
|
+
prices: signal?.prices ?? null,
|
|
2820
|
+
trendShiftContext: additional.trendShiftContext ?? null,
|
|
2821
|
+
marketContext: additional.marketContext ?? null
|
|
2822
|
+
};
|
|
2823
|
+
return (0, import_crypto.createHash)("sha1").update(stableStringify(context)).digest("hex");
|
|
2824
|
+
};
|
|
2825
|
+
var summarizeAiTrainDuplicateSignals = (rows) => {
|
|
2826
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2827
|
+
for (const row of rows) {
|
|
2828
|
+
const strategy = normalizeKeyPart(row.strategyName, "unknown");
|
|
2829
|
+
const symbol = normalizeKeyPart(row.symbol, "unknown");
|
|
2830
|
+
const direction = normalizeKeyPart(row.direction, "UNKNOWN");
|
|
2831
|
+
const timestamp = Number(row.timestamp);
|
|
2832
|
+
const profit = Number(row.profit);
|
|
2833
|
+
const key = [
|
|
2834
|
+
strategy,
|
|
2835
|
+
symbol,
|
|
2836
|
+
direction,
|
|
2837
|
+
Number.isFinite(timestamp) ? timestamp : "no_timestamp",
|
|
2838
|
+
Number.isFinite(profit) ? profit : "no_profit",
|
|
2839
|
+
hashContext(row)
|
|
2840
|
+
].join("|");
|
|
2841
|
+
const group = groups.get(key) ?? {
|
|
2842
|
+
key,
|
|
2843
|
+
strategy,
|
|
2844
|
+
symbol,
|
|
2845
|
+
direction,
|
|
2846
|
+
timestamp: Number.isFinite(timestamp) ? timestamp : 0,
|
|
2847
|
+
count: 0,
|
|
2848
|
+
totalProfit: 0
|
|
2849
|
+
};
|
|
2850
|
+
group.count += 1;
|
|
2851
|
+
group.totalProfit += Number.isFinite(profit) ? profit : 0;
|
|
2852
|
+
groups.set(key, group);
|
|
2853
|
+
}
|
|
2854
|
+
const duplicateGroups = [...groups.values()].filter((group) => group.count > 1).sort(
|
|
2855
|
+
(left, right) => left.totalProfit - right.totalProfit || right.count - left.count || left.key.localeCompare(right.key)
|
|
2856
|
+
);
|
|
2857
|
+
return {
|
|
2858
|
+
groups: duplicateGroups.length,
|
|
2859
|
+
rows: duplicateGroups.reduce((total, group) => total + group.count, 0),
|
|
2860
|
+
maxGroupSize: duplicateGroups.reduce(
|
|
2861
|
+
(max, group) => Math.max(max, group.count),
|
|
2862
|
+
0
|
|
2863
|
+
),
|
|
2864
|
+
worstGroups: duplicateGroups.slice(0, 10)
|
|
2865
|
+
};
|
|
2866
|
+
};
|
|
2867
|
+
|
|
2868
|
+
// src/lib/aiTrainDataset.ts
|
|
2869
|
+
var extractSignalFromAiDatasetRow = (row) => {
|
|
2870
|
+
const { payload } = row;
|
|
2871
|
+
return {
|
|
2872
|
+
...payload.signal,
|
|
2873
|
+
strategy: payload.signal.strategy,
|
|
2874
|
+
figures: payload.figures ?? {},
|
|
2875
|
+
indicators: payload.indicators ?? {},
|
|
2876
|
+
additionalIndicators: payload.additionalIndicators ?? {},
|
|
2877
|
+
prices: payload.signal.prices
|
|
2878
|
+
};
|
|
2879
|
+
};
|
|
1778
2880
|
|
|
1779
2881
|
// src/scripts/aiTrain.ts
|
|
1780
2882
|
var ListIt = require_list_it();
|
|
@@ -1791,38 +2893,103 @@ import_args.default.option(
|
|
|
1791
2893
|
50
|
|
1792
2894
|
);
|
|
1793
2895
|
import_args.default.option(
|
|
1794
|
-
"skip",
|
|
2896
|
+
["k", "skip"],
|
|
1795
2897
|
"How many recent trades to skip from the end before selecting replay rows",
|
|
1796
2898
|
0
|
|
1797
2899
|
);
|
|
1798
2900
|
import_args.default.option(
|
|
1799
2901
|
["p", "parallel"],
|
|
1800
2902
|
"Concurrent AI requests during replay",
|
|
1801
|
-
|
|
2903
|
+
import_constants2.AI_CONCURRENCY_LIMIT
|
|
1802
2904
|
);
|
|
1803
2905
|
import_args.default.option(
|
|
1804
|
-
"model",
|
|
2906
|
+
["m", "model"],
|
|
1805
2907
|
"OpenRouter model id override for this replay run",
|
|
1806
2908
|
import_ai2.DEFAULT_AI_MODEL
|
|
1807
2909
|
);
|
|
1808
|
-
import_args.default.option("minQuality", "Minimum AI quality required to approve entry", 4);
|
|
1809
2910
|
import_args.default.option(
|
|
1810
|
-
"
|
|
2911
|
+
["M", "minQuality"],
|
|
2912
|
+
"Minimum AI quality required to approve entry",
|
|
2913
|
+
4
|
|
2914
|
+
);
|
|
2915
|
+
import_args.default.option(
|
|
2916
|
+
["l", "localOnly"],
|
|
1811
2917
|
"Replay deterministic adapter gate without AI provider calls",
|
|
1812
2918
|
false
|
|
1813
2919
|
);
|
|
1814
|
-
import_args.default.option("
|
|
2920
|
+
import_args.default.option(["U", "user"], "Use user config", "root");
|
|
2921
|
+
import_args.default.option(
|
|
2922
|
+
["c", "chart"],
|
|
2923
|
+
"Save compact AI approval chart data for the strategies UI",
|
|
2924
|
+
false
|
|
2925
|
+
);
|
|
2926
|
+
import_args.default.option(["j", "json"], "Print structured JSON summary", false);
|
|
2927
|
+
import_args.default.option(
|
|
2928
|
+
["S", "since"],
|
|
2929
|
+
"Only evaluate rows at or after this timestamp (ISO date or epoch ms)",
|
|
2930
|
+
""
|
|
2931
|
+
);
|
|
2932
|
+
import_args.default.option(
|
|
2933
|
+
["u", "until"],
|
|
2934
|
+
"Only evaluate rows at or before this timestamp (ISO date or epoch ms)",
|
|
2935
|
+
""
|
|
2936
|
+
);
|
|
2937
|
+
import_args.default.option(
|
|
2938
|
+
["P", "period"],
|
|
2939
|
+
"Evaluate a trailing selected-row period such as last365d, last90d, or last30d",
|
|
2940
|
+
""
|
|
2941
|
+
);
|
|
2942
|
+
import_args.default.option(
|
|
2943
|
+
["q", "qualityThresholds"],
|
|
2944
|
+
"Comma-separated qN+ thresholds to summarize",
|
|
2945
|
+
"3,4,5"
|
|
2946
|
+
);
|
|
2947
|
+
import_args.default.option(
|
|
2948
|
+
["W", "terminalWindows"],
|
|
2949
|
+
"Comma-separated terminal dataset windows in days; use --terminalWindows=90,30,7",
|
|
2950
|
+
"90,30,7"
|
|
2951
|
+
);
|
|
2952
|
+
import_args.default.option(
|
|
2953
|
+
["d", "dumpEvaluations"],
|
|
2954
|
+
"Write evaluated rows as JSONL for offline pocket research",
|
|
2955
|
+
""
|
|
2956
|
+
);
|
|
2957
|
+
import_args.default.option(
|
|
2958
|
+
["G", "dumpFeatures"],
|
|
2959
|
+
"Feature snapshot to include in --dumpEvaluations rows: none, gateFeatures, or baseContext",
|
|
2960
|
+
"none"
|
|
2961
|
+
);
|
|
2962
|
+
import_args.default.option(
|
|
2963
|
+
["Q", "symbolQuarantine"],
|
|
2964
|
+
"Apply ordered per-strategy/per-symbol quarantine overlay to approved rows",
|
|
2965
|
+
false
|
|
2966
|
+
);
|
|
2967
|
+
import_args.default.option(
|
|
2968
|
+
["L", "symbolQuarantineMinLosses"],
|
|
2969
|
+
"Approved losses required before symbol quarantine can trigger",
|
|
2970
|
+
5
|
|
2971
|
+
);
|
|
2972
|
+
import_args.default.option(
|
|
2973
|
+
["F", "symbolQuarantineMinProfitFactor"],
|
|
2974
|
+
"Minimum symbol profit factor required to avoid quarantine",
|
|
2975
|
+
1
|
|
2976
|
+
);
|
|
2977
|
+
import_args.default.option(
|
|
2978
|
+
["D", "symbolQuarantineDays"],
|
|
2979
|
+
"How many days to keep a symbol quarantined after trigger",
|
|
2980
|
+
14
|
|
2981
|
+
);
|
|
1815
2982
|
var flags = import_args.default.parse(process.argv);
|
|
1816
2983
|
var createListIt = () => new ListIt({
|
|
1817
2984
|
autoAlign: true,
|
|
1818
2985
|
headerUnderline: true
|
|
1819
2986
|
});
|
|
1820
2987
|
var createTable = (headers, rows) => createListIt().setHeaderRow(headers).d(rows).toString();
|
|
1821
|
-
var
|
|
2988
|
+
var formatRatio2 = (value) => value == null ? "n/a" : `${(value * 100).toFixed(1)}%`;
|
|
1822
2989
|
var formatProfit = (value) => value == null ? "n/a" : value.toFixed(2);
|
|
1823
2990
|
var formatMetricNumber = (value) => value == null ? "n/a" : value.toFixed(2);
|
|
1824
2991
|
var colorizeRatio = (value) => {
|
|
1825
|
-
const text =
|
|
2992
|
+
const text = formatRatio2(value);
|
|
1826
2993
|
if (value == null) {
|
|
1827
2994
|
return import_chalk.default.gray(text);
|
|
1828
2995
|
}
|
|
@@ -1876,6 +3043,10 @@ var normalizePositiveInt = (value, fallback) => {
|
|
|
1876
3043
|
const parsed = Number(value);
|
|
1877
3044
|
return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : fallback;
|
|
1878
3045
|
};
|
|
3046
|
+
var normalizeNonNegativeNumber = (value, fallback) => {
|
|
3047
|
+
const parsed = Number(value);
|
|
3048
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
3049
|
+
};
|
|
1879
3050
|
var normalizeQuality = (analysis) => {
|
|
1880
3051
|
const quality = Number(analysis?.quality);
|
|
1881
3052
|
return Number.isFinite(quality) ? Math.round(quality) : null;
|
|
@@ -1889,30 +3060,81 @@ var listMergedFiles = async (params) => {
|
|
|
1889
3060
|
const { outDir, strategyName } = params;
|
|
1890
3061
|
let entries = [];
|
|
1891
3062
|
try {
|
|
1892
|
-
entries = await
|
|
3063
|
+
entries = await import_promises2.default.readdir(outDir);
|
|
1893
3064
|
} catch {
|
|
1894
3065
|
return [];
|
|
1895
3066
|
}
|
|
1896
3067
|
const prefix = strategyName ? `ai-dataset-${(0, import_ai.toFileToken)(strategyName)}-merged-` : "ai-dataset-";
|
|
1897
|
-
return entries.filter((name) => name.startsWith(prefix)).filter((name) =>
|
|
3068
|
+
return entries.filter((name) => name.startsWith(prefix)).filter((name) => {
|
|
3069
|
+
if (!name.endsWith(".jsonl") || !name.includes("-merged-")) {
|
|
3070
|
+
return false;
|
|
3071
|
+
}
|
|
3072
|
+
return /-merged-\d+\.jsonl$/.test(name) || /-merged-\d+-part\d+\.jsonl$/.test(name);
|
|
3073
|
+
}).sort().map((name) => import_path.default.join(outDir, name));
|
|
1898
3074
|
};
|
|
1899
3075
|
var deriveStrategyNameFromFile = (filePath) => {
|
|
1900
|
-
const match = import_path.default.basename(filePath).match(/^ai-dataset-(.+)-merged-\d
|
|
3076
|
+
const match = import_path.default.basename(filePath).match(/^ai-dataset-(.+)-merged-\d+(?:-part\d+)?\.jsonl$/);
|
|
1901
3077
|
return match?.[1] ? match[1] : "unknown";
|
|
1902
3078
|
};
|
|
1903
|
-
var
|
|
1904
|
-
const
|
|
3079
|
+
var getMergedGroupId = (filePath) => {
|
|
3080
|
+
const match = import_path.default.basename(filePath).match(/^ai-dataset-(.+)-merged-(\d+)(?:-part(\d+))?\.jsonl$/);
|
|
3081
|
+
if (!match) {
|
|
3082
|
+
return null;
|
|
3083
|
+
}
|
|
1905
3084
|
return {
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
figures: payload.figures ?? {},
|
|
1909
|
-
indicators: payload.indicators ?? {},
|
|
1910
|
-
additionalIndicators: payload.additionalIndicators ?? {},
|
|
1911
|
-
prices: payload.signal.prices
|
|
3085
|
+
strategyToken: match[1],
|
|
3086
|
+
mergeId: match[2]
|
|
1912
3087
|
};
|
|
1913
3088
|
};
|
|
3089
|
+
var sortDatasetPartPaths = (filePaths) => [...filePaths].sort((left, right) => {
|
|
3090
|
+
const leftMatch = import_path.default.basename(left).match(/-part(\d+)\.jsonl$/);
|
|
3091
|
+
const rightMatch = import_path.default.basename(right).match(/-part(\d+)\.jsonl$/);
|
|
3092
|
+
const leftPart = leftMatch ? Number(leftMatch[1]) : 0;
|
|
3093
|
+
const rightPart = rightMatch ? Number(rightMatch[1]) : 0;
|
|
3094
|
+
return leftPart - rightPart || left.localeCompare(right);
|
|
3095
|
+
});
|
|
3096
|
+
var resolveMergedDatasetFiles = async ({
|
|
3097
|
+
outDir,
|
|
3098
|
+
strategyName,
|
|
3099
|
+
explicitFile
|
|
3100
|
+
}) => {
|
|
3101
|
+
const mergedFiles = await listMergedFiles({
|
|
3102
|
+
outDir,
|
|
3103
|
+
strategyName
|
|
3104
|
+
});
|
|
3105
|
+
if (!mergedFiles.length) {
|
|
3106
|
+
throw new Error(
|
|
3107
|
+
strategyName ? `No merged AI dataset found for strategy "${strategyName}" in ${outDir}. Run yarn ai-export first.` : `No merged AI dataset found in ${outDir}. Run yarn ai-export first.`
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
const resolvedExplicitFile = explicitFile ? import_path.default.resolve(explicitFile) : null;
|
|
3111
|
+
if (resolvedExplicitFile) {
|
|
3112
|
+
const groupId = getMergedGroupId(resolvedExplicitFile);
|
|
3113
|
+
if (!groupId) {
|
|
3114
|
+
return [resolvedExplicitFile];
|
|
3115
|
+
}
|
|
3116
|
+
const groupedFiles = sortDatasetPartPaths(
|
|
3117
|
+
mergedFiles.filter((candidate) => {
|
|
3118
|
+
const candidateGroup = getMergedGroupId(candidate);
|
|
3119
|
+
return candidateGroup?.strategyToken === groupId.strategyToken && candidateGroup?.mergeId === groupId.mergeId;
|
|
3120
|
+
})
|
|
3121
|
+
);
|
|
3122
|
+
return groupedFiles.length ? groupedFiles : [resolvedExplicitFile];
|
|
3123
|
+
}
|
|
3124
|
+
const latestFile = mergedFiles[mergedFiles.length - 1];
|
|
3125
|
+
const latestGroupId = getMergedGroupId(latestFile);
|
|
3126
|
+
if (!latestGroupId) {
|
|
3127
|
+
return [latestFile];
|
|
3128
|
+
}
|
|
3129
|
+
return sortDatasetPartPaths(
|
|
3130
|
+
mergedFiles.filter((candidate) => {
|
|
3131
|
+
const candidateGroup = getMergedGroupId(candidate);
|
|
3132
|
+
return candidateGroup?.strategyToken === latestGroupId.strategyToken && candidateGroup?.mergeId === latestGroupId.mergeId;
|
|
3133
|
+
})
|
|
3134
|
+
);
|
|
3135
|
+
};
|
|
1914
3136
|
var resolvePromptRunContext = (row) => {
|
|
1915
|
-
const signal =
|
|
3137
|
+
const signal = extractSignalFromAiDatasetRow(row);
|
|
1916
3138
|
return {
|
|
1917
3139
|
signal,
|
|
1918
3140
|
promptPair: (0, import_ai2.buildAiPrompts)(signal)
|
|
@@ -1993,71 +3215,152 @@ var printSection = (title, table) => {
|
|
|
1993
3215
|
console.log(table);
|
|
1994
3216
|
console.log("");
|
|
1995
3217
|
};
|
|
1996
|
-
var
|
|
3218
|
+
var persistAiChartSnapshot = async (params) => {
|
|
3219
|
+
const {
|
|
3220
|
+
strategyName,
|
|
3221
|
+
evaluatedRows,
|
|
3222
|
+
model,
|
|
3223
|
+
mode,
|
|
3224
|
+
userName,
|
|
3225
|
+
minQuality,
|
|
3226
|
+
datasetId
|
|
3227
|
+
} = params;
|
|
3228
|
+
const generatedAt = Date.now();
|
|
3229
|
+
const runLabel = mode === model ? "" : `${mode}:${model}`;
|
|
3230
|
+
const snapshot = buildAiChartSnapshot({
|
|
3231
|
+
evaluatedRows,
|
|
3232
|
+
strategyName,
|
|
3233
|
+
generatedAt,
|
|
3234
|
+
runLabel,
|
|
3235
|
+
minQuality,
|
|
3236
|
+
datasetId
|
|
3237
|
+
});
|
|
3238
|
+
await Promise.all(
|
|
3239
|
+
snapshot.strategies.map(
|
|
3240
|
+
(card) => (0, import_redis.setData)(import_redis.redisKeys.strategyChartCard(userName, "ai", card.cardId), card, {
|
|
3241
|
+
expire: import_constants.TTL_1M
|
|
3242
|
+
})
|
|
3243
|
+
)
|
|
3244
|
+
);
|
|
3245
|
+
return snapshot;
|
|
3246
|
+
};
|
|
3247
|
+
var resolveDatasetFiles = async () => {
|
|
1997
3248
|
const explicitFile = String(flags.file || "").trim();
|
|
1998
|
-
if (explicitFile) {
|
|
1999
|
-
return import_path.default.resolve(explicitFile);
|
|
2000
|
-
}
|
|
2001
3249
|
const outDir = String(flags.outDir || "data/ai/export");
|
|
2002
3250
|
const strategyName = String(flags.strategy || "").trim() || void 0;
|
|
2003
|
-
|
|
3251
|
+
return resolveMergedDatasetFiles({
|
|
2004
3252
|
outDir,
|
|
2005
|
-
strategyName
|
|
3253
|
+
strategyName,
|
|
3254
|
+
explicitFile
|
|
2006
3255
|
});
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
3256
|
+
};
|
|
3257
|
+
var getDatasetRowTimestamp = (row) => {
|
|
3258
|
+
const timestamp = Number(row.timestamp);
|
|
3259
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
3260
|
+
};
|
|
3261
|
+
var findMaxSelectedTimestamp = async ({
|
|
3262
|
+
filePaths,
|
|
3263
|
+
recent,
|
|
3264
|
+
skip
|
|
3265
|
+
}) => {
|
|
3266
|
+
let maxTimestamp = null;
|
|
3267
|
+
await (0, import_ai.streamAiDatasetRows)({
|
|
3268
|
+
filePaths,
|
|
3269
|
+
limitFromEnd: recent,
|
|
3270
|
+
skipFromEnd: skip,
|
|
3271
|
+
onRow: async (row) => {
|
|
3272
|
+
const timestamp = getDatasetRowTimestamp(row);
|
|
3273
|
+
if (timestamp != null) {
|
|
3274
|
+
maxTimestamp = maxTimestamp == null ? timestamp : Math.max(maxTimestamp, timestamp);
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
});
|
|
3278
|
+
return maxTimestamp;
|
|
2013
3279
|
};
|
|
2014
3280
|
var main = async () => {
|
|
2015
|
-
const recent = normalizeInt(flags.recent, 50);
|
|
2016
3281
|
const skip = normalizeInt(flags.skip, 0);
|
|
2017
3282
|
const minQuality = normalizeInt(flags.minQuality, 4);
|
|
2018
3283
|
const localOnly = Boolean(flags.localOnly);
|
|
3284
|
+
const saveChart = Boolean(flags.chart);
|
|
2019
3285
|
const jsonOutput = Boolean(flags.json);
|
|
3286
|
+
const sinceInput = parseTimestampFilter(flags.since);
|
|
3287
|
+
const untilInput = parseTimestampFilter(flags.until);
|
|
3288
|
+
const trailingPeriodMs = parseTrailingPeriodMs(flags.period);
|
|
3289
|
+
const periodLabel = String(flags.period || "").trim() || null;
|
|
3290
|
+
const hasDateFilter = trailingPeriodMs != null || sinceInput != null || untilInput != null;
|
|
3291
|
+
const recent = resolveAiTrainRecentLimit({
|
|
3292
|
+
argv: process.argv,
|
|
3293
|
+
recentValue: flags.recent,
|
|
3294
|
+
hasDateFilter
|
|
3295
|
+
});
|
|
3296
|
+
const qualityThresholds = parseQualityThresholds(flags.qualityThresholds);
|
|
3297
|
+
const terminalWindowDays = parseTerminalWindowDays(flags.terminalWindows);
|
|
3298
|
+
const dumpEvaluationsPath = String(flags.dumpEvaluations || "").trim();
|
|
3299
|
+
const dumpFeatureMode = parseDumpFeatureMode(flags.dumpFeatures);
|
|
3300
|
+
if (dumpFeatureMode !== "none" && !dumpEvaluationsPath) {
|
|
3301
|
+
throw new Error("--dumpFeatures requires --dumpEvaluations.");
|
|
3302
|
+
}
|
|
3303
|
+
const symbolQuarantineEnabled = Boolean(flags.symbolQuarantine);
|
|
3304
|
+
const symbolQuarantineMinLosses = normalizePositiveInt(
|
|
3305
|
+
flags.symbolQuarantineMinLosses,
|
|
3306
|
+
5
|
|
3307
|
+
);
|
|
3308
|
+
const symbolQuarantineMinProfitFactor = normalizeNonNegativeNumber(
|
|
3309
|
+
flags.symbolQuarantineMinProfitFactor,
|
|
3310
|
+
1
|
|
3311
|
+
);
|
|
3312
|
+
const symbolQuarantineDays = normalizeNonNegativeNumber(
|
|
3313
|
+
flags.symbolQuarantineDays,
|
|
3314
|
+
14
|
|
3315
|
+
);
|
|
3316
|
+
const userName = String(flags.user || "root").trim() || "root";
|
|
2020
3317
|
const model = String(flags.model || import_ai2.DEFAULT_AI_MODEL).trim() || import_ai2.DEFAULT_AI_MODEL;
|
|
2021
|
-
const parallel = normalizePositiveInt(flags.parallel,
|
|
3318
|
+
const parallel = normalizePositiveInt(flags.parallel, import_constants2.AI_CONCURRENCY_LIMIT);
|
|
2022
3319
|
await (0, import_ai2.ensureAiStrategyPluginsLoaded)();
|
|
2023
|
-
const
|
|
2024
|
-
const
|
|
2025
|
-
|
|
3320
|
+
const filePaths = await resolveDatasetFiles();
|
|
3321
|
+
const datasetId = getMergedGroupId(filePaths[0] || "")?.mergeId;
|
|
3322
|
+
const { totalRows, selectedRows } = await (0, import_ai.countAiDatasetRows)({
|
|
3323
|
+
filePaths,
|
|
2026
3324
|
limitFromEnd: recent,
|
|
2027
3325
|
skipFromEnd: skip
|
|
2028
3326
|
});
|
|
2029
|
-
|
|
3327
|
+
const maxSelectedTimestamp = trailingPeriodMs == null ? null : await findMaxSelectedTimestamp({ filePaths, recent, skip });
|
|
3328
|
+
const sinceTimestamp = trailingPeriodMs != null && maxSelectedTimestamp != null ? maxSelectedTimestamp - trailingPeriodMs : sinceInput;
|
|
3329
|
+
const untilTimestamp = untilInput;
|
|
3330
|
+
if (!selectedRows) {
|
|
2030
3331
|
console.log(
|
|
2031
3332
|
import_chalk.default.yellow(
|
|
2032
|
-
`No AI prompt rows selected in ${
|
|
3333
|
+
`No AI prompt rows selected in ${filePaths.join(", ")} (recent=${recent || "all"}, skip=${skip})`
|
|
2033
3334
|
)
|
|
2034
3335
|
);
|
|
2035
3336
|
process.exit(0);
|
|
2036
3337
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
const payload = (0, import_ai2.buildAiPayload)(signal);
|
|
2041
|
-
return {
|
|
2042
|
-
row,
|
|
2043
|
-
promptPair,
|
|
2044
|
-
payload,
|
|
2045
|
-
signal,
|
|
2046
|
-
deterministic: resolveDeterministicGateEvaluation(signal, row.direction)
|
|
2047
|
-
};
|
|
2048
|
-
});
|
|
2049
|
-
const concurrency = Math.max(1, Math.min(parallel, rows.length));
|
|
3338
|
+
let strategyName = deriveStrategyNameFromFile(filePaths[0] || "");
|
|
3339
|
+
let strategyNameResolvedFromRow = false;
|
|
3340
|
+
const concurrency = Math.max(1, Math.min(parallel, selectedRows));
|
|
2050
3341
|
const bar = jsonOutput ? null : new import_progress.default(":current/:total [:bar][:percent] :symbol :status", {
|
|
2051
|
-
total:
|
|
3342
|
+
total: selectedRows,
|
|
2052
3343
|
width: 20
|
|
2053
3344
|
});
|
|
2054
3345
|
let failed = 0;
|
|
3346
|
+
let scanned = 0;
|
|
3347
|
+
let dateSkipped = 0;
|
|
2055
3348
|
const errorMessages = [];
|
|
2056
3349
|
const evaluations = [];
|
|
2057
|
-
|
|
2058
|
-
|
|
3350
|
+
const evaluatedRows = [];
|
|
3351
|
+
const duplicateSignalRows = [];
|
|
3352
|
+
const deterministicEvaluations = [];
|
|
3353
|
+
const activeTasks = /* @__PURE__ */ new Set();
|
|
3354
|
+
const processRow = async (row, sequence) => {
|
|
3355
|
+
const { promptPair, signal } = resolvePromptRunContext(row);
|
|
3356
|
+
const payload = (0, import_ai2.buildAiPayload)(signal);
|
|
3357
|
+
const deterministic = resolveDeterministicGateEvaluation(
|
|
3358
|
+
signal,
|
|
3359
|
+
row.direction
|
|
3360
|
+
);
|
|
2059
3361
|
const profit = Number(row.profit);
|
|
2060
3362
|
const profitableTrade = isProfitableTrade(row);
|
|
3363
|
+
deterministicEvaluations.push(deterministic);
|
|
2061
3364
|
try {
|
|
2062
3365
|
const analysis = localOnly ? await (0, import_ai2.runAiPromptLocal)(signal, { payload }) : await (0, import_ai2.runAiPrompt)(
|
|
2063
3366
|
promptPair,
|
|
@@ -2069,16 +3372,54 @@ var main = async () => {
|
|
|
2069
3372
|
);
|
|
2070
3373
|
const aiApproved = isAiApproval(row, analysis, minQuality);
|
|
2071
3374
|
const quality = normalizeQuality(analysis);
|
|
3375
|
+
const timestamp = Number.isFinite(Number(row.timestamp)) ? Number(row.timestamp) : null;
|
|
3376
|
+
const modelDirection = typeof analysis.direction === "string" && analysis.direction.trim() ? analysis.direction : null;
|
|
3377
|
+
const rejectReason = typeof analysis.rejectReason === "string" && analysis.rejectReason ? String(analysis.rejectReason) : null;
|
|
3378
|
+
const modelDirectionMatches = modelDirection === row.direction;
|
|
2072
3379
|
const isCorrect = aiApproved === profitableTrade;
|
|
3380
|
+
const features = buildAiTrainEvaluationFeatureSnapshot({
|
|
3381
|
+
additionalIndicators: payload.additionalIndicators,
|
|
3382
|
+
mode: dumpFeatureMode
|
|
3383
|
+
});
|
|
2073
3384
|
evaluations.push({
|
|
3385
|
+
signalId: row.signalId,
|
|
2074
3386
|
profit,
|
|
2075
3387
|
profitableTrade,
|
|
2076
3388
|
aiApproved,
|
|
3389
|
+
rawAiApproved: aiApproved,
|
|
2077
3390
|
quality,
|
|
2078
3391
|
direction: row.direction,
|
|
2079
|
-
|
|
2080
|
-
|
|
3392
|
+
modelDirection,
|
|
3393
|
+
modelDirectionMatches,
|
|
3394
|
+
timestamp,
|
|
3395
|
+
modelCandidate: deterministic.modelCandidate,
|
|
3396
|
+
strategy: row.strategyName,
|
|
3397
|
+
symbol: row.symbol,
|
|
3398
|
+
testName: row.testName?.trim() || "",
|
|
3399
|
+
configId: row.configId?.trim() || "",
|
|
3400
|
+
rejectReason,
|
|
3401
|
+
sequence,
|
|
3402
|
+
...features ? { features } : {}
|
|
2081
3403
|
});
|
|
3404
|
+
if (saveChart) {
|
|
3405
|
+
evaluatedRows.push({
|
|
3406
|
+
signalId: row.signalId,
|
|
3407
|
+
symbol: row.symbol,
|
|
3408
|
+
testName: row.testName?.trim() || "",
|
|
3409
|
+
configId: row.configId?.trim() || "",
|
|
3410
|
+
modelDirection,
|
|
3411
|
+
profit,
|
|
3412
|
+
profitableTrade,
|
|
3413
|
+
aiApproved,
|
|
3414
|
+
quality,
|
|
3415
|
+
direction: row.direction,
|
|
3416
|
+
timestamp,
|
|
3417
|
+
strategy: row.strategyName,
|
|
3418
|
+
rawAiApproved: aiApproved,
|
|
3419
|
+
sequence,
|
|
3420
|
+
tradeResult: row.tradeResult
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
2082
3423
|
bar?.tick(1, {
|
|
2083
3424
|
symbol: import_chalk.default.gray(row.symbol),
|
|
2084
3425
|
status: isCorrect ? import_chalk.default.green("ok") : import_chalk.default.red("miss")
|
|
@@ -2095,43 +3436,183 @@ var main = async () => {
|
|
|
2095
3436
|
status: import_chalk.default.yellow("error")
|
|
2096
3437
|
});
|
|
2097
3438
|
}
|
|
3439
|
+
};
|
|
3440
|
+
await (0, import_ai.streamAiDatasetRows)({
|
|
3441
|
+
filePaths,
|
|
3442
|
+
limitFromEnd: recent,
|
|
3443
|
+
skipFromEnd: skip,
|
|
3444
|
+
onRow: async (row) => {
|
|
3445
|
+
scanned += 1;
|
|
3446
|
+
if (!strategyNameResolvedFromRow && typeof row.strategyName === "string" && row.strategyName.trim()) {
|
|
3447
|
+
strategyName = row.strategyName.trim();
|
|
3448
|
+
strategyNameResolvedFromRow = true;
|
|
3449
|
+
}
|
|
3450
|
+
const timestamp = getDatasetRowTimestamp(row);
|
|
3451
|
+
if (sinceTimestamp != null && (timestamp == null || timestamp < sinceTimestamp) || untilTimestamp != null && (timestamp == null || timestamp > untilTimestamp)) {
|
|
3452
|
+
dateSkipped += 1;
|
|
3453
|
+
bar?.tick(1, {
|
|
3454
|
+
symbol: import_chalk.default.gray(row.symbol),
|
|
3455
|
+
status: import_chalk.default.gray("date-skip")
|
|
3456
|
+
});
|
|
3457
|
+
return;
|
|
3458
|
+
}
|
|
3459
|
+
duplicateSignalRows.push(row);
|
|
3460
|
+
const task = processRow(row, duplicateSignalRows.length - 1).finally(
|
|
3461
|
+
() => {
|
|
3462
|
+
activeTasks.delete(task);
|
|
3463
|
+
}
|
|
3464
|
+
);
|
|
3465
|
+
activeTasks.add(task);
|
|
3466
|
+
if (activeTasks.size >= concurrency) {
|
|
3467
|
+
await Promise.race(activeTasks);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
});
|
|
3471
|
+
await Promise.all(activeTasks);
|
|
3472
|
+
const quarantine = applyAiTrainSymbolQuarantine(evaluations, {
|
|
3473
|
+
enabled: symbolQuarantineEnabled,
|
|
3474
|
+
minApprovedLosses: symbolQuarantineMinLosses,
|
|
3475
|
+
minProfitFactor: symbolQuarantineMinProfitFactor,
|
|
3476
|
+
cooldownDays: symbolQuarantineDays
|
|
2098
3477
|
});
|
|
2099
|
-
const
|
|
2100
|
-
const
|
|
3478
|
+
const finalEvaluations = quarantine.evaluations;
|
|
3479
|
+
const duplicateSummary = summarizeAiTrainDuplicateSignals(duplicateSignalRows);
|
|
3480
|
+
if (saveChart && symbolQuarantineEnabled) {
|
|
3481
|
+
const approvalByKey = new Map(
|
|
3482
|
+
finalEvaluations.map((evaluation) => [
|
|
3483
|
+
evaluation.sequence,
|
|
3484
|
+
evaluation.aiApproved
|
|
3485
|
+
])
|
|
3486
|
+
);
|
|
3487
|
+
for (const row of evaluatedRows) {
|
|
3488
|
+
if (row.sequence != null) {
|
|
3489
|
+
row.aiApproved = approvalByKey.get(row.sequence) ?? row.aiApproved;
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
const summary = summarizeAiTrainEvaluations(finalEvaluations);
|
|
3494
|
+
const directionSummaries = summarizeAiTrainEvaluationsByDirection(finalEvaluations);
|
|
3495
|
+
const monthlySummaries = summarizeAiTrainEvaluationsByMonth(finalEvaluations);
|
|
3496
|
+
const qualityThresholdSummaries = summarizeAiTrainEvaluationsByQualityThreshold(
|
|
3497
|
+
finalEvaluations,
|
|
3498
|
+
qualityThresholds
|
|
3499
|
+
);
|
|
2101
3500
|
const deterministicSummary = summarizeDeterministicGateEvaluations(
|
|
2102
|
-
|
|
2103
|
-
|
|
3501
|
+
deterministicEvaluations,
|
|
3502
|
+
finalEvaluations.map((evaluation) => ({
|
|
3503
|
+
aiApproved: evaluation.rawAiApproved ?? evaluation.aiApproved,
|
|
3504
|
+
modelCandidate: evaluation.modelCandidate
|
|
3505
|
+
}))
|
|
3506
|
+
);
|
|
3507
|
+
const coverage = summarizeAiTrainCoverage(finalEvaluations);
|
|
3508
|
+
const terminalWindows = summarizeAiTrainTerminalWindows(
|
|
3509
|
+
finalEvaluations,
|
|
3510
|
+
terminalWindowDays
|
|
2104
3511
|
);
|
|
3512
|
+
const topRejectReasons = summarizeAiTrainRejectReasons(finalEvaluations);
|
|
3513
|
+
const lineage = await buildAiTrainLineage({
|
|
3514
|
+
projectRoot: String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd(),
|
|
3515
|
+
strategyName,
|
|
3516
|
+
configIds: finalEvaluations.map((evaluation) => evaluation.configId),
|
|
3517
|
+
runContext: {
|
|
3518
|
+
mode: localOnly ? "local-deterministic" : "llm",
|
|
3519
|
+
model: localOnly ? "local-deterministic" : model,
|
|
3520
|
+
minQuality
|
|
3521
|
+
}
|
|
3522
|
+
});
|
|
2105
3523
|
const evaluated = summary.correct + summary.incorrect;
|
|
2106
3524
|
const result = {
|
|
2107
3525
|
run: {
|
|
2108
3526
|
strategy: strategyName,
|
|
2109
|
-
|
|
2110
|
-
|
|
3527
|
+
datasetId: datasetId ?? null,
|
|
3528
|
+
shardCount: filePaths.length,
|
|
3529
|
+
filePath: filePaths.join(","),
|
|
3530
|
+
selected: finalEvaluations.length,
|
|
2111
3531
|
sourceRows: totalRows,
|
|
3532
|
+
scanned,
|
|
3533
|
+
dateSkipped,
|
|
2112
3534
|
recent,
|
|
2113
3535
|
skip,
|
|
3536
|
+
since: sinceTimestamp,
|
|
3537
|
+
until: untilTimestamp,
|
|
3538
|
+
period: periodLabel,
|
|
2114
3539
|
minQuality,
|
|
2115
3540
|
mode: localOnly ? "local-deterministic" : "llm",
|
|
2116
3541
|
model: localOnly ? "local-deterministic" : model,
|
|
2117
|
-
parallel: concurrency
|
|
3542
|
+
parallel: concurrency,
|
|
3543
|
+
dumpFeatures: dumpFeatureMode,
|
|
3544
|
+
terminalWindows: terminalWindowDays
|
|
2118
3545
|
},
|
|
2119
3546
|
outcome: summary,
|
|
2120
3547
|
byDirection: directionSummaries,
|
|
3548
|
+
monthly: monthlySummaries,
|
|
3549
|
+
qualityThresholds: qualityThresholdSummaries,
|
|
2121
3550
|
deterministicFlow: deterministicSummary,
|
|
2122
3551
|
qualityBreakdown: summary.qualityBuckets,
|
|
3552
|
+
symbolQuarantine: quarantine.summary,
|
|
3553
|
+
duplicates: duplicateSummary,
|
|
3554
|
+
research: {
|
|
3555
|
+
coverage,
|
|
3556
|
+
terminalWindows,
|
|
3557
|
+
topRejectReasons,
|
|
3558
|
+
lineage
|
|
3559
|
+
},
|
|
2123
3560
|
errors: {
|
|
2124
3561
|
failed,
|
|
2125
3562
|
providerErrors: errorMessages
|
|
2126
3563
|
}
|
|
2127
3564
|
};
|
|
3565
|
+
if (dumpEvaluationsPath) {
|
|
3566
|
+
await import_promises2.default.mkdir(import_path.default.dirname(import_path.default.resolve(dumpEvaluationsPath)), {
|
|
3567
|
+
recursive: true
|
|
3568
|
+
});
|
|
3569
|
+
await import_promises2.default.writeFile(
|
|
3570
|
+
dumpEvaluationsPath,
|
|
3571
|
+
finalEvaluations.map(
|
|
3572
|
+
(evaluation) => JSON.stringify({
|
|
3573
|
+
signalId: evaluation.signalId,
|
|
3574
|
+
strategy: evaluation.strategy,
|
|
3575
|
+
symbol: evaluation.symbol,
|
|
3576
|
+
direction: evaluation.direction,
|
|
3577
|
+
modelDirection: evaluation.modelDirection,
|
|
3578
|
+
modelDirectionMatches: evaluation.modelDirectionMatches,
|
|
3579
|
+
timestamp: evaluation.timestamp,
|
|
3580
|
+
testName: evaluation.testName,
|
|
3581
|
+
configId: evaluation.configId,
|
|
3582
|
+
profit: evaluation.profit,
|
|
3583
|
+
profitableTrade: evaluation.profitableTrade,
|
|
3584
|
+
aiApproved: evaluation.aiApproved,
|
|
3585
|
+
rawAiApproved: evaluation.rawAiApproved,
|
|
3586
|
+
quality: evaluation.quality,
|
|
3587
|
+
modelCandidate: evaluation.modelCandidate,
|
|
3588
|
+
rejectReason: evaluation.rejectReason,
|
|
3589
|
+
sequence: evaluation.sequence,
|
|
3590
|
+
...evaluation.features ? { features: evaluation.features } : {}
|
|
3591
|
+
})
|
|
3592
|
+
).join("\n") + (finalEvaluations.length ? "\n" : ""),
|
|
3593
|
+
"utf8"
|
|
3594
|
+
);
|
|
3595
|
+
}
|
|
3596
|
+
if (saveChart) {
|
|
3597
|
+
await persistAiChartSnapshot({
|
|
3598
|
+
strategyName,
|
|
3599
|
+
evaluatedRows,
|
|
3600
|
+
minQuality,
|
|
3601
|
+
model: localOnly ? "local-deterministic" : model,
|
|
3602
|
+
mode: localOnly ? "local-deterministic" : "llm",
|
|
3603
|
+
userName,
|
|
3604
|
+
datasetId
|
|
3605
|
+
});
|
|
3606
|
+
}
|
|
2128
3607
|
if (jsonOutput) {
|
|
2129
3608
|
console.log(JSON.stringify(result));
|
|
2130
|
-
process.exit(failed ===
|
|
3609
|
+
process.exit(failed === selectedRows ? 1 : 0);
|
|
2131
3610
|
}
|
|
2132
3611
|
console.log("");
|
|
2133
3612
|
console.log(import_chalk.default.green("AI train finished"));
|
|
2134
|
-
|
|
3613
|
+
filePaths.forEach((filePath) => {
|
|
3614
|
+
console.log(import_chalk.default.gray(filePath));
|
|
3615
|
+
});
|
|
2135
3616
|
console.log("");
|
|
2136
3617
|
printSection(
|
|
2137
3618
|
"RUN",
|
|
@@ -2139,17 +3620,69 @@ var main = async () => {
|
|
|
2139
3620
|
[import_chalk.default.gray("FIELD"), import_chalk.default.gray("VALUE")],
|
|
2140
3621
|
[
|
|
2141
3622
|
["strategy", import_chalk.default.yellow(strategyName)],
|
|
2142
|
-
["
|
|
3623
|
+
["dataset_id", import_chalk.default.gray(datasetId ?? "n/a")],
|
|
3624
|
+
["dataset_shards", import_chalk.default.blue(String(filePaths.length))],
|
|
3625
|
+
["selected", import_chalk.default.blue(String(finalEvaluations.length))],
|
|
2143
3626
|
["source_rows", import_chalk.default.blue(String(totalRows))],
|
|
3627
|
+
["scanned", import_chalk.default.blue(String(scanned))],
|
|
3628
|
+
["date_skipped", import_chalk.default.blue(String(dateSkipped))],
|
|
2144
3629
|
["recent", import_chalk.default.blue(recent === 0 ? "all" : String(recent))],
|
|
2145
3630
|
["skip", import_chalk.default.blue(String(skip))],
|
|
3631
|
+
[
|
|
3632
|
+
"since",
|
|
3633
|
+
sinceTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(new Date(sinceTimestamp).toISOString())
|
|
3634
|
+
],
|
|
3635
|
+
[
|
|
3636
|
+
"until",
|
|
3637
|
+
untilTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(new Date(untilTimestamp).toISOString())
|
|
3638
|
+
],
|
|
3639
|
+
[
|
|
3640
|
+
"period",
|
|
3641
|
+
periodLabel == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(periodLabel)
|
|
3642
|
+
],
|
|
2146
3643
|
["min_quality", import_chalk.default.magenta(String(minQuality))],
|
|
2147
3644
|
[
|
|
2148
3645
|
"mode",
|
|
2149
3646
|
localOnly ? import_chalk.default.green("local-deterministic") : import_chalk.default.yellow("llm")
|
|
2150
3647
|
],
|
|
2151
3648
|
["model", import_chalk.default.yellow(localOnly ? "local-deterministic" : model)],
|
|
2152
|
-
["parallel", import_chalk.default.magenta(String(concurrency))]
|
|
3649
|
+
["parallel", import_chalk.default.magenta(String(concurrency))],
|
|
3650
|
+
[
|
|
3651
|
+
"terminal_windows",
|
|
3652
|
+
terminalWindowDays.length ? import_chalk.default.gray(terminalWindowDays.map((days) => `${days}d`).join(",")) : import_chalk.default.gray("off")
|
|
3653
|
+
],
|
|
3654
|
+
[
|
|
3655
|
+
"dataset_min_timestamp",
|
|
3656
|
+
coverage.minTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(new Date(coverage.minTimestamp).toISOString())
|
|
3657
|
+
],
|
|
3658
|
+
[
|
|
3659
|
+
"dataset_max_timestamp",
|
|
3660
|
+
coverage.maxTimestamp == null ? import_chalk.default.gray("n/a") : import_chalk.default.gray(new Date(coverage.maxTimestamp).toISOString())
|
|
3661
|
+
],
|
|
3662
|
+
[
|
|
3663
|
+
"dataset_lag_days",
|
|
3664
|
+
coverage.dataLagDays == null ? import_chalk.default.gray("n/a") : import_chalk.default.yellow(coverage.dataLagDays.toFixed(2))
|
|
3665
|
+
],
|
|
3666
|
+
["git_sha", import_chalk.default.gray(lineage.gitSha ?? "n/a")],
|
|
3667
|
+
[
|
|
3668
|
+
"git_dirty",
|
|
3669
|
+
lineage.gitDirty == null ? import_chalk.default.gray("n/a") : lineage.gitDirty ? import_chalk.default.yellow("true") : import_chalk.default.green("false")
|
|
3670
|
+
],
|
|
3671
|
+
["gate_fingerprint", import_chalk.default.gray(lineage.gateFingerprint)],
|
|
3672
|
+
["config_ids_fingerprint", import_chalk.default.gray(lineage.configIdsFingerprint)],
|
|
3673
|
+
["context_fingerprint", import_chalk.default.gray(lineage.contextFingerprint)],
|
|
3674
|
+
[
|
|
3675
|
+
"symbol_quarantine",
|
|
3676
|
+
symbolQuarantineEnabled ? import_chalk.default.green("enabled") : import_chalk.default.gray("off")
|
|
3677
|
+
],
|
|
3678
|
+
[
|
|
3679
|
+
"dump_evaluations",
|
|
3680
|
+
dumpEvaluationsPath ? import_chalk.default.gray(dumpEvaluationsPath) : import_chalk.default.gray("off")
|
|
3681
|
+
],
|
|
3682
|
+
[
|
|
3683
|
+
"dump_features",
|
|
3684
|
+
dumpFeatureMode === "none" ? import_chalk.default.gray("off") : import_chalk.default.gray(dumpFeatureMode)
|
|
3685
|
+
]
|
|
2153
3686
|
]
|
|
2154
3687
|
)
|
|
2155
3688
|
);
|
|
@@ -2195,6 +3728,55 @@ var main = async () => {
|
|
|
2195
3728
|
]
|
|
2196
3729
|
)
|
|
2197
3730
|
);
|
|
3731
|
+
if (terminalWindows.length) {
|
|
3732
|
+
printSection(
|
|
3733
|
+
"TERMINAL WINDOWS (ANCHORED TO DATASET MAX)",
|
|
3734
|
+
createTable(
|
|
3735
|
+
[
|
|
3736
|
+
import_chalk.default.gray("WINDOW"),
|
|
3737
|
+
import_chalk.default.gray("COVERAGE"),
|
|
3738
|
+
import_chalk.default.gray("FROM"),
|
|
3739
|
+
import_chalk.default.gray("TO"),
|
|
3740
|
+
import_chalk.default.gray("EVAL"),
|
|
3741
|
+
import_chalk.default.gray("APPROVED"),
|
|
3742
|
+
import_chalk.default.gray("CALENDAR/D"),
|
|
3743
|
+
import_chalk.default.gray("WR"),
|
|
3744
|
+
import_chalk.default.gray("PF"),
|
|
3745
|
+
import_chalk.default.gray("PNL"),
|
|
3746
|
+
import_chalk.default.gray("MAX_DD"),
|
|
3747
|
+
import_chalk.default.gray("TOP REJECT")
|
|
3748
|
+
],
|
|
3749
|
+
terminalWindows.map((window2) => [
|
|
3750
|
+
import_chalk.default.yellow(window2.label),
|
|
3751
|
+
window2.complete ? import_chalk.default.green("full") : import_chalk.default.yellow("partial"),
|
|
3752
|
+
import_chalk.default.gray(new Date(window2.since).toISOString()),
|
|
3753
|
+
import_chalk.default.gray(new Date(window2.until).toISOString()),
|
|
3754
|
+
import_chalk.default.blue(String(window2.selected)),
|
|
3755
|
+
import_chalk.default.cyan(String(window2.outcome.approved)),
|
|
3756
|
+
colorizeMetricNumber(window2.approvedPerCalendarDay),
|
|
3757
|
+
colorizeRatio(window2.outcome.approvedRisk.winRate),
|
|
3758
|
+
colorizeMetricNumber(window2.outcome.approvedRisk.profitFactor),
|
|
3759
|
+
colorizeProfit(window2.outcome.approvedRisk.totalProfit),
|
|
3760
|
+
colorizeProfit(-window2.outcome.approvedRisk.maxDrawdown),
|
|
3761
|
+
import_chalk.default.gray(
|
|
3762
|
+
window2.topRejectReasons[0] ? `${window2.topRejectReasons[0].reason} (${window2.topRejectReasons[0].count})` : "n/a"
|
|
3763
|
+
)
|
|
3764
|
+
])
|
|
3765
|
+
)
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
if (topRejectReasons.length) {
|
|
3769
|
+
printSection(
|
|
3770
|
+
"TOP REJECT REASONS",
|
|
3771
|
+
createTable(
|
|
3772
|
+
[import_chalk.default.gray("REASON"), import_chalk.default.gray("COUNT")],
|
|
3773
|
+
topRejectReasons.map(({ reason, count }) => [
|
|
3774
|
+
import_chalk.default.yellow(reason),
|
|
3775
|
+
import_chalk.default.blue(String(count))
|
|
3776
|
+
])
|
|
3777
|
+
)
|
|
3778
|
+
);
|
|
3779
|
+
}
|
|
2198
3780
|
printSection(
|
|
2199
3781
|
"CONFUSION",
|
|
2200
3782
|
createTable(
|
|
@@ -2209,6 +3791,56 @@ var main = async () => {
|
|
|
2209
3791
|
]
|
|
2210
3792
|
)
|
|
2211
3793
|
);
|
|
3794
|
+
printSection(
|
|
3795
|
+
"APPROVED RISK",
|
|
3796
|
+
createTable(
|
|
3797
|
+
[import_chalk.default.gray("METRIC"), import_chalk.default.gray("VALUE")],
|
|
3798
|
+
[
|
|
3799
|
+
["trades", import_chalk.default.cyan(String(summary.approvedRisk.trades))],
|
|
3800
|
+
["total_profit", colorizeProfit(summary.approvedRisk.totalProfit)],
|
|
3801
|
+
["gross_profit", colorizeProfit(summary.approvedRisk.grossProfit)],
|
|
3802
|
+
["gross_loss", colorizeProfit(-summary.approvedRisk.grossLoss)],
|
|
3803
|
+
[
|
|
3804
|
+
"profit_factor",
|
|
3805
|
+
colorizeMetricNumber(summary.approvedRisk.profitFactor)
|
|
3806
|
+
],
|
|
3807
|
+
[
|
|
3808
|
+
"payoff_ratio",
|
|
3809
|
+
colorizeMetricNumber(summary.approvedRisk.payoffRatio)
|
|
3810
|
+
],
|
|
3811
|
+
["avg_win", colorizeProfit(summary.approvedRisk.avgWin)],
|
|
3812
|
+
[
|
|
3813
|
+
"avg_loss",
|
|
3814
|
+
summary.approvedRisk.avgLoss == null ? import_chalk.default.gray("n/a") : colorizeProfit(-summary.approvedRisk.avgLoss)
|
|
3815
|
+
],
|
|
3816
|
+
["largest_win", colorizeProfit(summary.approvedRisk.largestWin)],
|
|
3817
|
+
["largest_loss", colorizeProfit(summary.approvedRisk.largestLoss)],
|
|
3818
|
+
["win_rate", colorizeRatio(summary.approvedRisk.winRate)],
|
|
3819
|
+
["max_drawdown", colorizeProfit(-summary.approvedRisk.maxDrawdown)],
|
|
3820
|
+
[
|
|
3821
|
+
"max_drawdown_pct_of_gross_profit",
|
|
3822
|
+
colorizeRatio(summary.approvedRisk.maxDrawdownPctOfGrossProfit)
|
|
3823
|
+
],
|
|
3824
|
+
[
|
|
3825
|
+
"max_drawdown_pct_of_total_profit",
|
|
3826
|
+
colorizeRatio(summary.approvedRisk.maxDrawdownPctOfTotalProfit)
|
|
3827
|
+
],
|
|
3828
|
+
[
|
|
3829
|
+
"recovery_factor",
|
|
3830
|
+
colorizeMetricNumber(summary.approvedRisk.recoveryFactor)
|
|
3831
|
+
],
|
|
3832
|
+
["ulcer_index", colorizeMetricNumber(summary.approvedRisk.ulcerIndex)],
|
|
3833
|
+
[
|
|
3834
|
+
"max_consecutive_wins",
|
|
3835
|
+
import_chalk.default.green(String(summary.approvedRisk.maxConsecutiveWins))
|
|
3836
|
+
],
|
|
3837
|
+
[
|
|
3838
|
+
"max_consecutive_losses",
|
|
3839
|
+
import_chalk.default.red(String(summary.approvedRisk.maxConsecutiveLosses))
|
|
3840
|
+
]
|
|
3841
|
+
]
|
|
3842
|
+
)
|
|
3843
|
+
);
|
|
2212
3844
|
if (directionSummaries.length) {
|
|
2213
3845
|
printSection(
|
|
2214
3846
|
"BY DIRECTION",
|
|
@@ -2218,6 +3850,11 @@ var main = async () => {
|
|
|
2218
3850
|
import_chalk.default.gray("EVAL"),
|
|
2219
3851
|
import_chalk.default.gray("ACCURACY"),
|
|
2220
3852
|
import_chalk.default.gray("APPROVED"),
|
|
3853
|
+
import_chalk.default.gray("CADENCE/D"),
|
|
3854
|
+
import_chalk.default.gray("WR"),
|
|
3855
|
+
import_chalk.default.gray("PF"),
|
|
3856
|
+
import_chalk.default.gray("PNL"),
|
|
3857
|
+
import_chalk.default.gray("MAX_DD"),
|
|
2221
3858
|
import_chalk.default.green("TP"),
|
|
2222
3859
|
import_chalk.default.red("FP"),
|
|
2223
3860
|
import_chalk.default.green("TN"),
|
|
@@ -2232,6 +3869,11 @@ var main = async () => {
|
|
|
2232
3869
|
import_chalk.default.blue(String(directionEvaluated)),
|
|
2233
3870
|
colorizePercent(directionSummary.correct, directionEvaluated),
|
|
2234
3871
|
import_chalk.default.cyan(String(directionSummary.approved)),
|
|
3872
|
+
colorizeMetricNumber(directionSummary.avgApprovedTradesPerDay),
|
|
3873
|
+
colorizeRatio(directionSummary.approvedRisk.winRate),
|
|
3874
|
+
colorizeMetricNumber(directionSummary.approvedRisk.profitFactor),
|
|
3875
|
+
colorizeProfit(directionSummary.approvedRisk.totalProfit),
|
|
3876
|
+
colorizeProfit(-directionSummary.approvedRisk.maxDrawdown),
|
|
2235
3877
|
import_chalk.default.green(String(directionSummary.truePositive)),
|
|
2236
3878
|
import_chalk.default.red(String(directionSummary.falsePositive)),
|
|
2237
3879
|
import_chalk.default.green(String(directionSummary.trueNegative)),
|
|
@@ -2243,6 +3885,69 @@ var main = async () => {
|
|
|
2243
3885
|
)
|
|
2244
3886
|
);
|
|
2245
3887
|
}
|
|
3888
|
+
if (qualityThresholdSummaries.length) {
|
|
3889
|
+
printSection(
|
|
3890
|
+
"QUALITY THRESHOLDS",
|
|
3891
|
+
createTable(
|
|
3892
|
+
[
|
|
3893
|
+
import_chalk.default.gray("BUCKET"),
|
|
3894
|
+
import_chalk.default.gray("APPROVED"),
|
|
3895
|
+
import_chalk.default.gray("CADENCE/D"),
|
|
3896
|
+
import_chalk.default.gray("WR"),
|
|
3897
|
+
import_chalk.default.gray("PF"),
|
|
3898
|
+
import_chalk.default.gray("PNL"),
|
|
3899
|
+
import_chalk.default.gray("MAX_DD")
|
|
3900
|
+
],
|
|
3901
|
+
qualityThresholdSummaries.map(
|
|
3902
|
+
({ label, summary: thresholdSummary }) => [
|
|
3903
|
+
import_chalk.default.yellow(label),
|
|
3904
|
+
import_chalk.default.cyan(String(thresholdSummary.approved)),
|
|
3905
|
+
colorizeMetricNumber(thresholdSummary.avgApprovedTradesPerDay),
|
|
3906
|
+
colorizeRatio(thresholdSummary.approvedRisk.winRate),
|
|
3907
|
+
colorizeMetricNumber(thresholdSummary.approvedRisk.profitFactor),
|
|
3908
|
+
colorizeProfit(thresholdSummary.approvedRisk.totalProfit),
|
|
3909
|
+
colorizeProfit(-thresholdSummary.approvedRisk.maxDrawdown)
|
|
3910
|
+
]
|
|
3911
|
+
)
|
|
3912
|
+
)
|
|
3913
|
+
);
|
|
3914
|
+
}
|
|
3915
|
+
if (monthlySummaries.length) {
|
|
3916
|
+
const losingMonths = monthlySummaries.filter(
|
|
3917
|
+
({ summary: monthSummary }) => monthSummary.approvedRisk.totalProfit < 0
|
|
3918
|
+
).length;
|
|
3919
|
+
printSection(
|
|
3920
|
+
"MONTHLY STABILITY",
|
|
3921
|
+
createTable(
|
|
3922
|
+
[
|
|
3923
|
+
import_chalk.default.gray("MONTH"),
|
|
3924
|
+
import_chalk.default.gray("APPROVED"),
|
|
3925
|
+
import_chalk.default.gray("WR"),
|
|
3926
|
+
import_chalk.default.gray("PF"),
|
|
3927
|
+
import_chalk.default.gray("PNL"),
|
|
3928
|
+
import_chalk.default.gray("MAX_DD")
|
|
3929
|
+
],
|
|
3930
|
+
[
|
|
3931
|
+
[
|
|
3932
|
+
import_chalk.default.yellow("losing_months"),
|
|
3933
|
+
import_chalk.default.cyan(`${losingMonths}/${monthlySummaries.length}`),
|
|
3934
|
+
import_chalk.default.gray(""),
|
|
3935
|
+
import_chalk.default.gray(""),
|
|
3936
|
+
import_chalk.default.gray(""),
|
|
3937
|
+
import_chalk.default.gray("")
|
|
3938
|
+
],
|
|
3939
|
+
...monthlySummaries.map(({ month, summary: monthSummary }) => [
|
|
3940
|
+
import_chalk.default.yellow(month),
|
|
3941
|
+
import_chalk.default.cyan(String(monthSummary.approved)),
|
|
3942
|
+
colorizeRatio(monthSummary.approvedRisk.winRate),
|
|
3943
|
+
colorizeMetricNumber(monthSummary.approvedRisk.profitFactor),
|
|
3944
|
+
colorizeProfit(monthSummary.approvedRisk.totalProfit),
|
|
3945
|
+
colorizeProfit(-monthSummary.approvedRisk.maxDrawdown)
|
|
3946
|
+
])
|
|
3947
|
+
]
|
|
3948
|
+
)
|
|
3949
|
+
);
|
|
3950
|
+
}
|
|
2246
3951
|
printSection(
|
|
2247
3952
|
"DETERMINISTIC FLOW",
|
|
2248
3953
|
createTable(
|
|
@@ -2284,6 +3989,82 @@ var main = async () => {
|
|
|
2284
3989
|
]
|
|
2285
3990
|
)
|
|
2286
3991
|
);
|
|
3992
|
+
printSection(
|
|
3993
|
+
"SYMBOL QUARANTINE",
|
|
3994
|
+
createTable(
|
|
3995
|
+
[import_chalk.default.gray("FIELD"), import_chalk.default.gray("VALUE")],
|
|
3996
|
+
[
|
|
3997
|
+
[
|
|
3998
|
+
"enabled",
|
|
3999
|
+
quarantine.summary.enabled ? import_chalk.default.green("true") : import_chalk.default.gray("false")
|
|
4000
|
+
],
|
|
4001
|
+
[
|
|
4002
|
+
"min_approved_losses",
|
|
4003
|
+
import_chalk.default.cyan(String(quarantine.summary.minApprovedLosses))
|
|
4004
|
+
],
|
|
4005
|
+
[
|
|
4006
|
+
"min_profit_factor",
|
|
4007
|
+
import_chalk.default.cyan(String(quarantine.summary.minProfitFactor))
|
|
4008
|
+
],
|
|
4009
|
+
["cooldown_days", import_chalk.default.cyan(String(quarantine.summary.cooldownDays))],
|
|
4010
|
+
["blocked", import_chalk.default.yellow(String(quarantine.summary.blocked))],
|
|
4011
|
+
["events", import_chalk.default.yellow(String(quarantine.summary.events.length))]
|
|
4012
|
+
]
|
|
4013
|
+
)
|
|
4014
|
+
);
|
|
4015
|
+
if (quarantine.summary.events.length) {
|
|
4016
|
+
printSection(
|
|
4017
|
+
"SYMBOL QUARANTINE EVENTS",
|
|
4018
|
+
createTable(
|
|
4019
|
+
[
|
|
4020
|
+
import_chalk.default.gray("SYMBOL"),
|
|
4021
|
+
import_chalk.default.gray("STARTED"),
|
|
4022
|
+
import_chalk.default.gray("UNTIL"),
|
|
4023
|
+
import_chalk.default.gray("LOSSES"),
|
|
4024
|
+
import_chalk.default.gray("PF")
|
|
4025
|
+
],
|
|
4026
|
+
quarantine.summary.events.slice(0, 20).map((event) => [
|
|
4027
|
+
import_chalk.default.yellow(event.symbol),
|
|
4028
|
+
import_chalk.default.gray(new Date(event.startedAt).toISOString()),
|
|
4029
|
+
import_chalk.default.gray(new Date(event.until).toISOString()),
|
|
4030
|
+
import_chalk.default.red(String(event.approvedLosses)),
|
|
4031
|
+
colorizeMetricNumber(event.profitFactor)
|
|
4032
|
+
])
|
|
4033
|
+
)
|
|
4034
|
+
);
|
|
4035
|
+
}
|
|
4036
|
+
printSection(
|
|
4037
|
+
"DUPLICATE SIGNALS",
|
|
4038
|
+
createTable(
|
|
4039
|
+
[import_chalk.default.gray("FIELD"), import_chalk.default.gray("VALUE")],
|
|
4040
|
+
[
|
|
4041
|
+
["groups", import_chalk.default.yellow(String(duplicateSummary.groups))],
|
|
4042
|
+
["rows", import_chalk.default.yellow(String(duplicateSummary.rows))],
|
|
4043
|
+
["max_group_size", import_chalk.default.yellow(String(duplicateSummary.maxGroupSize))]
|
|
4044
|
+
]
|
|
4045
|
+
)
|
|
4046
|
+
);
|
|
4047
|
+
if (duplicateSummary.worstGroups.length) {
|
|
4048
|
+
printSection(
|
|
4049
|
+
"WORST DUPLICATE GROUPS",
|
|
4050
|
+
createTable(
|
|
4051
|
+
[
|
|
4052
|
+
import_chalk.default.gray("SYMBOL"),
|
|
4053
|
+
import_chalk.default.gray("DIR"),
|
|
4054
|
+
import_chalk.default.gray("TIME"),
|
|
4055
|
+
import_chalk.default.gray("COUNT"),
|
|
4056
|
+
import_chalk.default.gray("TOTAL_PROFIT")
|
|
4057
|
+
],
|
|
4058
|
+
duplicateSummary.worstGroups.map((group) => [
|
|
4059
|
+
import_chalk.default.yellow(group.symbol),
|
|
4060
|
+
import_chalk.default.gray(group.direction),
|
|
4061
|
+
import_chalk.default.gray(new Date(group.timestamp).toISOString()),
|
|
4062
|
+
import_chalk.default.cyan(String(group.count)),
|
|
4063
|
+
colorizeProfit(group.totalProfit)
|
|
4064
|
+
])
|
|
4065
|
+
)
|
|
4066
|
+
);
|
|
4067
|
+
}
|
|
2287
4068
|
if (summary.qualityBuckets.length) {
|
|
2288
4069
|
printSection(
|
|
2289
4070
|
"QUALITY BREAKDOWN",
|
|
@@ -2291,18 +4072,16 @@ var main = async () => {
|
|
|
2291
4072
|
[
|
|
2292
4073
|
import_chalk.default.gray("QUALITY"),
|
|
2293
4074
|
import_chalk.default.gray("COUNT"),
|
|
2294
|
-
import_chalk.default.gray("APPROVED"),
|
|
2295
|
-
import_chalk.default.gray("APPROVAL_RATE"),
|
|
2296
4075
|
import_chalk.default.gray("WINRATE"),
|
|
2297
|
-
import_chalk.default.gray("AVG_PROFIT")
|
|
4076
|
+
import_chalk.default.gray("AVG_PROFIT"),
|
|
4077
|
+
import_chalk.default.gray("TOTAL_PROFIT")
|
|
2298
4078
|
],
|
|
2299
4079
|
summary.qualityBuckets.map((bucket) => [
|
|
2300
4080
|
colorizeQuality(bucket.quality),
|
|
2301
4081
|
import_chalk.default.blue(String(bucket.count)),
|
|
2302
|
-
import_chalk.default.cyan(String(bucket.approved)),
|
|
2303
|
-
colorizeRatio(bucket.approved / bucket.count),
|
|
2304
4082
|
colorizeRatio(bucket.profitable / bucket.count),
|
|
2305
|
-
colorizeProfit(bucket.totalProfit / bucket.count)
|
|
4083
|
+
colorizeProfit(bucket.totalProfit / bucket.count),
|
|
4084
|
+
colorizeProfit(bucket.totalProfit)
|
|
2306
4085
|
])
|
|
2307
4086
|
)
|
|
2308
4087
|
);
|
|
@@ -2313,9 +4092,9 @@ var main = async () => {
|
|
|
2313
4092
|
console.log(import_chalk.default.yellow(`- ${message}`));
|
|
2314
4093
|
});
|
|
2315
4094
|
}
|
|
2316
|
-
process.exit(failed ===
|
|
4095
|
+
process.exit(failed === selectedRows ? 1 : 0);
|
|
2317
4096
|
};
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
4097
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4098
|
+
0 && (module.exports = {
|
|
4099
|
+
main
|
|
2321
4100
|
});
|