@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.
Files changed (76) hide show
  1. package/dist/cli.js +29748 -13641
  2. package/dist/lib/aiPocketSearch.js +1316 -0
  3. package/dist/lib/aiTrainCharts.js +711 -0
  4. package/dist/lib/aiTrainDataset.js +40 -0
  5. package/dist/lib/aiTrainEvaluationDump.js +76 -0
  6. package/dist/lib/aiTrainMetrics.js +148 -8
  7. package/dist/lib/aiTrainOptions.js +130 -0
  8. package/dist/lib/aiTrainQuarantine.js +199 -0
  9. package/dist/lib/aiTrainResearch.js +467 -0
  10. package/dist/lib/binanceMarketContextBackfill.js +841 -0
  11. package/dist/lib/binanceMarketData.js +392 -0
  12. package/dist/lib/coinMarketCapContextBackfill.js +1088 -0
  13. package/dist/lib/continuity.js +60 -0
  14. package/dist/lib/derivativesContextBackfill.js +490 -77
  15. package/dist/lib/executionCalibration.js +705 -0
  16. package/dist/lib/marketContextPrepare.js +4403 -0
  17. package/dist/lib/paritySummary.js +101 -0
  18. package/dist/lib/runBot.js +1 -0
  19. package/dist/lib/runEnvironment.js +2254 -0
  20. package/dist/lib/runFormatting.js +1657 -0
  21. package/dist/lib/runtimeDebugEvidence.js +526 -0
  22. package/dist/lib/runtimeModeConfig.js +53 -0
  23. package/dist/lib/runtimeParity.js +88 -11
  24. package/dist/lib/runtimeParityDetails.js +751 -0
  25. package/dist/lib/runtimeRedis.js +157 -0
  26. package/dist/lib/runtimeSignalsLoader.js +187 -0
  27. package/dist/lib/runtimeSignalsStorage.js +185 -0
  28. package/dist/lib/runtimeStrategyBacktest.js +134 -0
  29. package/dist/lib/runtimeTradeSync.js +263 -0
  30. package/dist/lib/telegramReports.js +9 -0
  31. package/dist/lib/tickerUniverseCache.js +116 -0
  32. package/dist/lib/timeWindow.js +3 -3
  33. package/dist/scripts/agentRun.js +28 -8
  34. package/dist/scripts/aiExport.js +1721 -7
  35. package/dist/scripts/aiExportSelect.js +16 -4
  36. package/dist/scripts/aiPocketSearch.js +4190 -0
  37. package/dist/scripts/aiTrain.js +1866 -87
  38. package/dist/scripts/backtest.js +4509 -818
  39. package/dist/scripts/binanceMarketIngest.js +565 -0
  40. package/dist/scripts/bot.js +16 -2
  41. package/dist/scripts/candlesMigrateProvider.js +32 -22
  42. package/dist/scripts/cleanDir.js +15 -2
  43. package/dist/scripts/cleanRedis.js +15 -2
  44. package/dist/scripts/cleanTests.js +38 -2
  45. package/dist/scripts/cleanupMarketContext.js +69 -0
  46. package/dist/scripts/continuity.js +148 -67
  47. package/dist/scripts/derivativesIngest.js +14 -4
  48. package/dist/scripts/derivativesIngestCoinalyzeAll.js +14 -4
  49. package/dist/scripts/doctor.js +14 -4
  50. package/dist/scripts/executionCalibration.js +792 -0
  51. package/dist/scripts/infraCommon.js +10 -0
  52. package/dist/scripts/infraDown.js +14 -4
  53. package/dist/scripts/infraInit.js +24 -4
  54. package/dist/scripts/infraUp.js +14 -4
  55. package/dist/scripts/marketWs.js +222 -0
  56. package/dist/scripts/migration.js +15 -2
  57. package/dist/scripts/mlExport.js +1698 -10
  58. package/dist/scripts/mlExportSelect.js +16 -4
  59. package/dist/scripts/mlInspect.js +13 -3
  60. package/dist/scripts/mlTrainLatestSelect.js +14 -4
  61. package/dist/scripts/replay.js +8824 -0
  62. package/dist/scripts/replayRunner.js +8827 -0
  63. package/dist/scripts/replayRuntimeEvidence.js +868 -0
  64. package/dist/scripts/researchAuto.js +289 -60
  65. package/dist/scripts/results.js +3 -3
  66. package/dist/scripts/runtimeEvidence.js +605 -0
  67. package/dist/scripts/runtimeParity.js +6360 -1004
  68. package/dist/scripts/serverHealth.js +588 -0
  69. package/dist/scripts/signals.js +5623 -486
  70. package/dist/scripts/signalsDaemon.js +6167 -0
  71. package/dist/scripts/signalsSummary.js +954 -312
  72. package/dist/scripts/test-ml.js +25 -3
  73. package/dist/scripts/test.js +26 -1
  74. package/dist/scripts/user-add.js +15 -2
  75. package/dist/workers/testerWorker.js +130 -21
  76. package/package.json +18 -12
@@ -8,6 +8,10 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __commonJS = (cb, mod) => function __require() {
9
9
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
10
  };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
11
15
  var __copyProps = (to, from, except, desc) => {
12
16
  if (from && typeof from === "object" || typeof from === "function") {
13
17
  for (let key of __getOwnPropNames(from))
@@ -24,6 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
28
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
29
  mod
26
30
  ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
32
 
28
33
  // ../../node_modules/debug/node_modules/ms/index.js
29
34
  var require_ms = __commonJS({
@@ -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 import_promises = __toESM(require("fs/promises"));
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 import_async = require("@tradejs/core/async");
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 import_constants = require("@tradejs/node/constants");
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
- const timestamps = evaluations.map(
1647
- (evaluation) => typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp) ? evaluation.timestamp : null
1648
- ).filter((timestamp) => timestamp != null);
1649
- if (!timestamps.length) {
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
- import_constants.AI_CONCURRENCY_LIMIT
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
- "localOnly",
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("json", "Print structured JSON summary", false);
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 formatRatio = (value) => value == null ? "n/a" : `${(value * 100).toFixed(1)}%`;
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 = formatRatio(value);
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 import_promises.default.readdir(outDir);
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) => name.includes("-merged-") && name.endsWith(".jsonl")).sort().map((name) => import_path.default.join(outDir, 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+\.jsonl$/);
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 extractSignalFromDatasetRow = (row) => {
1904
- const { payload } = row;
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
- ...payload.signal,
1907
- strategy: payload.signal.strategy,
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 = extractSignalFromDatasetRow(row);
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 resolveDatasetFile = async () => {
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
- const mergedFiles = await listMergedFiles({
3251
+ return resolveMergedDatasetFiles({
2004
3252
  outDir,
2005
- strategyName
3253
+ strategyName,
3254
+ explicitFile
2006
3255
  });
2007
- if (!mergedFiles.length) {
2008
- throw new Error(
2009
- 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.`
2010
- );
2011
- }
2012
- return mergedFiles[mergedFiles.length - 1];
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, import_constants.AI_CONCURRENCY_LIMIT);
3318
+ const parallel = normalizePositiveInt(flags.parallel, import_constants2.AI_CONCURRENCY_LIMIT);
2022
3319
  await (0, import_ai2.ensureAiStrategyPluginsLoaded)();
2023
- const filePath = await resolveDatasetFile();
2024
- const { rows, totalRows } = await (0, import_ai.readAiDatasetRows)({
2025
- filePath,
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
- if (!rows.length) {
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 ${filePath} (recent=${recent || "all"}, skip=${skip})`
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
- const strategyName = rows[0]?.strategyName || deriveStrategyNameFromFile(filePath);
2038
- const preparedRows = rows.map((row) => {
2039
- const { promptPair, signal } = resolvePromptRunContext(row);
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: rows.length,
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
- await (0, import_async.runWithConcurrency)(preparedRows, concurrency, async (preparedRow) => {
2058
- const { row, promptPair, payload, signal, deterministic } = preparedRow;
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
- timestamp: Number.isFinite(Number(row.timestamp)) ? Number(row.timestamp) : null,
2080
- modelCandidate: deterministic.modelCandidate
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 summary = summarizeAiTrainEvaluations(evaluations);
2100
- const directionSummaries = summarizeAiTrainEvaluationsByDirection(evaluations);
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
- preparedRows.map((preparedRow) => preparedRow.deterministic),
2103
- evaluations
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
- filePath,
2110
- selected: rows.length,
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 === rows.length ? 1 : 0);
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
- console.log(import_chalk.default.gray(filePath));
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
- ["selected", import_chalk.default.blue(String(rows.length))],
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 === rows.length ? 1 : 0);
4095
+ process.exit(failed === selectedRows ? 1 : 0);
2317
4096
  };
2318
- main().catch((error) => {
2319
- console.error(import_chalk.default.red(error?.message || String(error)));
2320
- process.exit(1);
4097
+ // Annotate the CommonJS export names for ESM import in node:
4098
+ 0 && (module.exports = {
4099
+ main
2321
4100
  });