@tradejs/cli 1.0.9 → 1.0.11

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 (77) hide show
  1. package/README.md +13 -4
  2. package/dist/cli.js +29531 -13520
  3. package/dist/lib/aiPocketSearch.js +1316 -0
  4. package/dist/lib/aiTrainCharts.js +711 -0
  5. package/dist/lib/aiTrainDataset.js +40 -0
  6. package/dist/lib/aiTrainEvaluationDump.js +76 -0
  7. package/dist/lib/aiTrainMetrics.js +148 -8
  8. package/dist/lib/aiTrainOptions.js +130 -0
  9. package/dist/lib/aiTrainQuarantine.js +199 -0
  10. package/dist/lib/aiTrainResearch.js +467 -0
  11. package/dist/lib/binanceMarketContextBackfill.js +841 -0
  12. package/dist/lib/binanceMarketData.js +392 -0
  13. package/dist/lib/coinMarketCapContextBackfill.js +1126 -0
  14. package/dist/lib/continuity.js +60 -0
  15. package/dist/lib/derivativesContextBackfill.js +490 -77
  16. package/dist/lib/executionCalibration.js +705 -0
  17. package/dist/lib/marketContextPrepare.js +4441 -0
  18. package/dist/lib/paritySummary.js +101 -0
  19. package/dist/lib/runBot.js +1 -0
  20. package/dist/lib/runEnvironment.js +2259 -0
  21. package/dist/lib/runFormatting.js +1657 -0
  22. package/dist/lib/runtimeDebugEvidence.js +526 -0
  23. package/dist/lib/runtimeModeConfig.js +53 -0
  24. package/dist/lib/runtimeParity.js +88 -11
  25. package/dist/lib/runtimeParityDetails.js +751 -0
  26. package/dist/lib/runtimeRedis.js +157 -0
  27. package/dist/lib/runtimeSignalsLoader.js +40 -6
  28. package/dist/lib/runtimeSignalsStorage.js +45 -22
  29. package/dist/lib/runtimeStrategyBacktest.js +134 -0
  30. package/dist/lib/runtimeTradeSync.js +263 -0
  31. package/dist/lib/telegramReports.js +9 -0
  32. package/dist/lib/tickerUniverseCache.js +116 -0
  33. package/dist/lib/timeWindow.js +3 -3
  34. package/dist/scripts/agentRun.js +14 -8
  35. package/dist/scripts/aiExport.js +1721 -7
  36. package/dist/scripts/aiExportSelect.js +16 -4
  37. package/dist/scripts/aiPocketSearch.js +4190 -0
  38. package/dist/scripts/aiTrain.js +1866 -87
  39. package/dist/scripts/backtest.js +4534 -803
  40. package/dist/scripts/binanceMarketIngest.js +565 -0
  41. package/dist/scripts/bot.js +16 -2
  42. package/dist/scripts/candlesMigrateProvider.js +32 -22
  43. package/dist/scripts/cleanDir.js +15 -2
  44. package/dist/scripts/cleanRedis.js +15 -2
  45. package/dist/scripts/cleanTests.js +38 -2
  46. package/dist/scripts/cleanupMarketContext.js +69 -0
  47. package/dist/scripts/continuity.js +148 -67
  48. package/dist/scripts/derivativesIngest.js +14 -4
  49. package/dist/scripts/derivativesIngestCoinalyzeAll.js +14 -4
  50. package/dist/scripts/doctor.js +14 -4
  51. package/dist/scripts/executionCalibration.js +792 -0
  52. package/dist/scripts/infraCommon.js +18 -5
  53. package/dist/scripts/infraDown.js +14 -4
  54. package/dist/scripts/infraInit.js +32 -9
  55. package/dist/scripts/infraUp.js +24 -6
  56. package/dist/scripts/marketWs.js +222 -0
  57. package/dist/scripts/migration.js +15 -2
  58. package/dist/scripts/mlExport.js +1698 -10
  59. package/dist/scripts/mlExportSelect.js +16 -4
  60. package/dist/scripts/mlInspect.js +13 -3
  61. package/dist/scripts/mlTrainLatestSelect.js +14 -4
  62. package/dist/scripts/replay.js +8867 -0
  63. package/dist/scripts/replayRunner.js +8870 -0
  64. package/dist/scripts/replayRuntimeEvidence.js +868 -0
  65. package/dist/scripts/researchAuto.js +286 -59
  66. package/dist/scripts/results.js +3 -3
  67. package/dist/scripts/runtimeEvidence.js +605 -0
  68. package/dist/scripts/runtimeParity.js +6400 -1043
  69. package/dist/scripts/serverHealth.js +588 -0
  70. package/dist/scripts/signals.js +5628 -511
  71. package/dist/scripts/signalsDaemon.js +6205 -0
  72. package/dist/scripts/signalsSummary.js +833 -248
  73. package/dist/scripts/test-ml.js +25 -3
  74. package/dist/scripts/test.js +26 -1
  75. package/dist/scripts/user-add.js +15 -2
  76. package/dist/workers/testerWorker.js +130 -21
  77. package/package.json +18 -12
@@ -0,0 +1,711 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/lib/aiTrainCharts.ts
21
+ var aiTrainCharts_exports = {};
22
+ __export(aiTrainCharts_exports, {
23
+ buildAiChartSnapshot: () => buildAiChartSnapshot
24
+ });
25
+ module.exports = __toCommonJS(aiTrainCharts_exports);
26
+
27
+ // src/lib/aiTrainMetrics.ts
28
+ var divideOrNull = (num, denom) => {
29
+ if (denom <= 0) {
30
+ return null;
31
+ }
32
+ return num / denom;
33
+ };
34
+ var DAY_MS = 24 * 60 * 60 * 1e3;
35
+ var DAYS_PER_WEEK = 7;
36
+ var DAYS_PER_MONTH = 30.4375;
37
+ var getEvaluationPeriodDays = (evaluations) => {
38
+ let minTimestamp = null;
39
+ let maxTimestamp = null;
40
+ for (const evaluation of evaluations) {
41
+ const timestamp = typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp) ? evaluation.timestamp : null;
42
+ if (timestamp == null) {
43
+ continue;
44
+ }
45
+ if (minTimestamp == null || timestamp < minTimestamp) {
46
+ minTimestamp = timestamp;
47
+ }
48
+ if (maxTimestamp == null || timestamp > maxTimestamp) {
49
+ maxTimestamp = timestamp;
50
+ }
51
+ }
52
+ if (minTimestamp == null || maxTimestamp == null) {
53
+ return null;
54
+ }
55
+ return Math.max((maxTimestamp - minTimestamp) / DAY_MS, 1);
56
+ };
57
+ var qualitySortKey = (quality) => quality == null ? Number.POSITIVE_INFINITY : quality;
58
+ var emptyRiskSummary = () => ({
59
+ trades: 0,
60
+ totalProfit: 0,
61
+ grossProfit: 0,
62
+ grossLoss: 0,
63
+ profitFactor: null,
64
+ payoffRatio: null,
65
+ avgWin: null,
66
+ avgLoss: null,
67
+ largestWin: null,
68
+ largestLoss: null,
69
+ winRate: null,
70
+ maxDrawdown: 0,
71
+ maxDrawdownPctOfGrossProfit: null,
72
+ maxDrawdownPctOfTotalProfit: null,
73
+ recoveryFactor: null,
74
+ ulcerIndex: null,
75
+ maxConsecutiveWins: 0,
76
+ maxConsecutiveLosses: 0
77
+ });
78
+ var summarizeApprovedRisk = (evaluations) => {
79
+ const approvedEvaluations = evaluations.filter((evaluation) => evaluation.aiApproved).sort((left, right) => {
80
+ const leftTimestamp = typeof left.timestamp === "number" && Number.isFinite(left.timestamp) ? left.timestamp : Number.POSITIVE_INFINITY;
81
+ const rightTimestamp = typeof right.timestamp === "number" && Number.isFinite(right.timestamp) ? right.timestamp : Number.POSITIVE_INFINITY;
82
+ return leftTimestamp - rightTimestamp;
83
+ });
84
+ if (!approvedEvaluations.length) {
85
+ return emptyRiskSummary();
86
+ }
87
+ let grossProfit = 0;
88
+ let grossLoss = 0;
89
+ let wins = 0;
90
+ let losses = 0;
91
+ let largestWin = null;
92
+ let largestLoss = null;
93
+ let equity = 0;
94
+ let peakEquity = 0;
95
+ let maxDrawdown = 0;
96
+ let drawdownSquares = 0;
97
+ let currentWinStreak = 0;
98
+ let currentLossStreak = 0;
99
+ let maxConsecutiveWins = 0;
100
+ let maxConsecutiveLosses = 0;
101
+ for (const evaluation of approvedEvaluations) {
102
+ const profit = evaluation.profit;
103
+ if (profit > 0) {
104
+ grossProfit += profit;
105
+ wins += 1;
106
+ largestWin = largestWin == null ? profit : Math.max(largestWin, profit);
107
+ currentWinStreak += 1;
108
+ currentLossStreak = 0;
109
+ } else if (profit < 0) {
110
+ grossLoss += Math.abs(profit);
111
+ losses += 1;
112
+ largestLoss = largestLoss == null ? profit : Math.min(largestLoss, profit);
113
+ currentLossStreak += 1;
114
+ currentWinStreak = 0;
115
+ } else {
116
+ currentWinStreak = 0;
117
+ currentLossStreak = 0;
118
+ }
119
+ maxConsecutiveWins = Math.max(maxConsecutiveWins, currentWinStreak);
120
+ maxConsecutiveLosses = Math.max(maxConsecutiveLosses, currentLossStreak);
121
+ equity += profit;
122
+ peakEquity = Math.max(peakEquity, equity);
123
+ const drawdown = Math.max(0, peakEquity - equity);
124
+ maxDrawdown = Math.max(maxDrawdown, drawdown);
125
+ drawdownSquares += drawdown * drawdown;
126
+ }
127
+ const totalProfit = grossProfit - grossLoss;
128
+ const avgWin = divideOrNull(grossProfit, wins);
129
+ const avgLoss = divideOrNull(grossLoss, losses);
130
+ return {
131
+ trades: approvedEvaluations.length,
132
+ totalProfit,
133
+ grossProfit,
134
+ grossLoss,
135
+ profitFactor: grossLoss > 0 ? grossProfit / grossLoss : null,
136
+ payoffRatio: avgWin != null && avgLoss != null && avgLoss > 0 ? avgWin / avgLoss : null,
137
+ avgWin,
138
+ avgLoss,
139
+ largestWin,
140
+ largestLoss,
141
+ winRate: divideOrNull(wins, approvedEvaluations.length),
142
+ maxDrawdown,
143
+ maxDrawdownPctOfGrossProfit: grossProfit > 0 ? maxDrawdown / grossProfit : null,
144
+ maxDrawdownPctOfTotalProfit: totalProfit > 0 ? maxDrawdown / totalProfit : null,
145
+ recoveryFactor: maxDrawdown > 0 ? totalProfit / maxDrawdown : null,
146
+ ulcerIndex: Math.sqrt(drawdownSquares / approvedEvaluations.length),
147
+ maxConsecutiveWins,
148
+ maxConsecutiveLosses
149
+ };
150
+ };
151
+ var summarizeAiTrainEvaluations = (evaluations) => {
152
+ let approved = 0;
153
+ let rejected = 0;
154
+ let correct = 0;
155
+ let incorrect = 0;
156
+ let profitable = 0;
157
+ let unprofitable = 0;
158
+ let flat = 0;
159
+ let truePositive = 0;
160
+ let falsePositive = 0;
161
+ let trueNegative = 0;
162
+ let falseNegative = 0;
163
+ let totalProfitAll = 0;
164
+ let totalProfitApproved = 0;
165
+ const bucketMap = /* @__PURE__ */ new Map();
166
+ for (const evaluation of evaluations) {
167
+ const { profit, profitableTrade, aiApproved, quality } = evaluation;
168
+ totalProfitAll += profit;
169
+ if (profitableTrade) {
170
+ profitable += 1;
171
+ } else if (profit < 0) {
172
+ unprofitable += 1;
173
+ } else {
174
+ flat += 1;
175
+ }
176
+ if (aiApproved) {
177
+ approved += 1;
178
+ totalProfitApproved += profit;
179
+ } else {
180
+ rejected += 1;
181
+ }
182
+ const isCorrect = aiApproved === profitableTrade;
183
+ if (isCorrect) {
184
+ correct += 1;
185
+ } else {
186
+ incorrect += 1;
187
+ }
188
+ if (aiApproved && profitableTrade) {
189
+ truePositive += 1;
190
+ } else if (aiApproved && !profitableTrade) {
191
+ falsePositive += 1;
192
+ } else if (!aiApproved && profitableTrade) {
193
+ falseNegative += 1;
194
+ } else {
195
+ trueNegative += 1;
196
+ }
197
+ const bucket = bucketMap.get(quality) ?? {
198
+ quality,
199
+ count: 0,
200
+ approved: 0,
201
+ profitable: 0,
202
+ totalProfit: 0
203
+ };
204
+ bucket.count += 1;
205
+ bucket.totalProfit += profit;
206
+ if (aiApproved) {
207
+ bucket.approved += 1;
208
+ }
209
+ if (profitableTrade) {
210
+ bucket.profitable += 1;
211
+ }
212
+ bucketMap.set(quality, bucket);
213
+ }
214
+ const avgProfitAll = divideOrNull(totalProfitAll, evaluations.length);
215
+ const avgProfitApproved = divideOrNull(totalProfitApproved, approved);
216
+ const periodDays = getEvaluationPeriodDays(evaluations);
217
+ const avgProfitApprovedPerDay = periodDays == null ? null : divideOrNull(totalProfitApproved, periodDays);
218
+ const avgApprovedTradesPerDay = periodDays == null ? null : divideOrNull(approved, periodDays);
219
+ const avgProfitApprovedPerMonth = avgProfitApprovedPerDay == null ? null : avgProfitApprovedPerDay * DAYS_PER_MONTH;
220
+ const avgApprovedTradesPerWeek = avgApprovedTradesPerDay == null ? null : avgApprovedTradesPerDay * DAYS_PER_WEEK;
221
+ const expectancyDelta = avgProfitAll == null || avgProfitApproved == null ? null : avgProfitApproved - avgProfitAll;
222
+ return {
223
+ approved,
224
+ rejected,
225
+ correct,
226
+ incorrect,
227
+ truePositive,
228
+ falsePositive,
229
+ trueNegative,
230
+ falseNegative,
231
+ profitable,
232
+ unprofitable,
233
+ flat,
234
+ precisionApproved: divideOrNull(truePositive, approved),
235
+ recallWinners: divideOrNull(truePositive, profitable),
236
+ avgProfitAll,
237
+ avgProfitApproved,
238
+ avgProfitApprovedPerDay,
239
+ avgProfitApprovedPerMonth,
240
+ avgApprovedTradesPerDay,
241
+ avgApprovedTradesPerWeek,
242
+ expectancyDelta,
243
+ approvedRisk: summarizeApprovedRisk(evaluations),
244
+ qualityBuckets: [...bucketMap.values()].sort(
245
+ (a, b) => qualitySortKey(a.quality) - qualitySortKey(b.quality)
246
+ )
247
+ };
248
+ };
249
+ var getDirectionSortKey = (direction) => {
250
+ if (direction === "LONG") {
251
+ return 0;
252
+ }
253
+ if (direction === "SHORT") {
254
+ return 1;
255
+ }
256
+ return 2;
257
+ };
258
+ var summarizeAiTrainEvaluationsByDirection = (evaluations) => {
259
+ const grouped = /* @__PURE__ */ new Map();
260
+ for (const evaluation of evaluations) {
261
+ const direction = typeof evaluation.direction === "string" && evaluation.direction.trim() ? evaluation.direction : "UNKNOWN";
262
+ const bucket = grouped.get(direction) ?? [];
263
+ bucket.push(evaluation);
264
+ grouped.set(direction, bucket);
265
+ }
266
+ return [...grouped.entries()].sort(
267
+ ([leftDirection], [rightDirection]) => getDirectionSortKey(leftDirection) - getDirectionSortKey(rightDirection) || leftDirection.localeCompare(rightDirection)
268
+ ).map(([direction, directionEvaluations]) => ({
269
+ direction,
270
+ summary: summarizeAiTrainEvaluations(directionEvaluations)
271
+ }));
272
+ };
273
+
274
+ // src/lib/aiTrainCharts.ts
275
+ var formatRatio = (value) => value == null ? "n/a" : `${(value * 100).toFixed(1)}%`;
276
+ var formatPercent = (value) => value == null ? "n/a" : `${(value * 100).toFixed(1)}%`;
277
+ var formatPercentValue = (value) => value == null ? "n/a" : `${value.toFixed(1)}%`;
278
+ var formatSigned = (value) => value == null ? "n/a" : `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
279
+ var formatNumber = (value) => value == null ? "n/a" : value.toFixed(2);
280
+ var formatUtcWindowTimestamp = (timestamp) => {
281
+ if (timestamp == null || !Number.isFinite(timestamp)) {
282
+ return "n/a";
283
+ }
284
+ return new Date(timestamp).toISOString().replace("T", " ").replace(".000Z", " UTC");
285
+ };
286
+ var resolveMetricTone = (value) => {
287
+ if (value == null) {
288
+ return "default";
289
+ }
290
+ if (value > 0) {
291
+ return "success";
292
+ }
293
+ if (value < 0) {
294
+ return "error";
295
+ }
296
+ return "neutral";
297
+ };
298
+ var buildAiChartMetrics = (params) => {
299
+ const { summary, maxDrawdownPercent } = params;
300
+ const metrics = [
301
+ {
302
+ id: "maxDrawdown",
303
+ label: "Max drawdown",
304
+ value: formatPercentValue(maxDrawdownPercent),
305
+ tone: maxDrawdownPercent == null ? "default" : maxDrawdownPercent > 0 ? "warning" : "success"
306
+ },
307
+ {
308
+ id: "approved",
309
+ label: "Approved",
310
+ value: String(summary.approved)
311
+ },
312
+ {
313
+ id: "accuracy",
314
+ label: "Accuracy",
315
+ value: formatPercent(
316
+ summary.correct + summary.incorrect > 0 ? summary.correct / (summary.correct + summary.incorrect) : null
317
+ )
318
+ },
319
+ {
320
+ id: "precision",
321
+ label: "Precision",
322
+ value: formatPercent(summary.precisionApproved)
323
+ },
324
+ {
325
+ id: "recall",
326
+ label: "Recall",
327
+ value: formatPercent(summary.recallWinners)
328
+ },
329
+ {
330
+ id: "pnl",
331
+ label: "P&L",
332
+ value: formatSigned(summary.approvedRisk.totalProfit),
333
+ tone: resolveMetricTone(summary.approvedRisk.totalProfit)
334
+ },
335
+ {
336
+ id: "monthlyPnl",
337
+ label: "Monthly P&L",
338
+ value: formatSigned(summary.avgProfitApprovedPerMonth),
339
+ tone: resolveMetricTone(summary.avgProfitApprovedPerMonth)
340
+ },
341
+ {
342
+ id: "approvedPerDay",
343
+ label: "Approved/Day",
344
+ value: formatNumber(summary.avgApprovedTradesPerDay)
345
+ },
346
+ {
347
+ id: "avgProfit",
348
+ label: "Avg Profit",
349
+ value: formatSigned(summary.avgProfitApproved),
350
+ tone: resolveMetricTone(summary.avgProfitApproved)
351
+ }
352
+ ];
353
+ return metrics;
354
+ };
355
+ var buildAiChartDetails = (params) => {
356
+ const { summary, rows } = params;
357
+ let windowStart = null;
358
+ let windowEnd = null;
359
+ for (const row of rows) {
360
+ const timestamp = row.timestamp != null && Number.isFinite(row.timestamp) ? row.timestamp : null;
361
+ if (timestamp == null) {
362
+ continue;
363
+ }
364
+ if (windowStart == null || timestamp < windowStart) {
365
+ windowStart = timestamp;
366
+ }
367
+ if (windowEnd == null || timestamp > windowEnd) {
368
+ windowEnd = timestamp;
369
+ }
370
+ }
371
+ const directionDetails = summarizeAiTrainEvaluationsByDirection(rows).filter(({ direction }) => direction === "LONG" || direction === "SHORT").flatMap(({ direction, summary: directionSummary }) => [
372
+ {
373
+ id: `direction:${direction}:approved`,
374
+ label: `${direction} approved`,
375
+ value: String(directionSummary.approved)
376
+ },
377
+ {
378
+ id: `direction:${direction}:precision`,
379
+ label: `${direction} precision`,
380
+ value: formatRatio(directionSummary.precisionApproved)
381
+ },
382
+ {
383
+ id: `direction:${direction}:monthlyPnl`,
384
+ label: `${direction} monthly_pnl`,
385
+ value: formatSigned(directionSummary.avgProfitApprovedPerMonth),
386
+ tone: resolveMetricTone(directionSummary.avgProfitApprovedPerMonth)
387
+ },
388
+ {
389
+ id: `direction:${direction}:pnl`,
390
+ label: `${direction} pnl`,
391
+ value: formatSigned(directionSummary.approvedRisk.totalProfit),
392
+ tone: resolveMetricTone(directionSummary.approvedRisk.totalProfit)
393
+ },
394
+ {
395
+ id: `direction:${direction}:avgProfit`,
396
+ label: `${direction} avg_profit`,
397
+ value: formatSigned(directionSummary.avgProfitApproved),
398
+ tone: resolveMetricTone(directionSummary.avgProfitApproved)
399
+ }
400
+ ]);
401
+ const symbolDetails = [
402
+ ...rows.reduce((grouped, row) => {
403
+ if (!row.symbol) {
404
+ return grouped;
405
+ }
406
+ const symbolRows = grouped.get(row.symbol) ?? [];
407
+ symbolRows.push(row);
408
+ grouped.set(row.symbol, symbolRows);
409
+ return grouped;
410
+ }, /* @__PURE__ */ new Map()).entries()
411
+ ].map(([symbol, symbolRows]) => {
412
+ const symbolSummary = summarizeAiTrainEvaluations(symbolRows);
413
+ return {
414
+ symbol,
415
+ summary: symbolSummary,
416
+ pnl: symbolSummary.approvedRisk.totalProfit
417
+ };
418
+ }).filter(({ summary: summary2 }) => summary2.approved > 0).sort(
419
+ (left, right) => Math.abs(right.pnl) - Math.abs(left.pnl) || right.pnl - left.pnl || left.symbol.localeCompare(right.symbol)
420
+ ).slice(0, 10).flatMap(({ symbol, summary: summary2, pnl }) => [
421
+ {
422
+ id: `symbol:${symbol}:pnl`,
423
+ label: `${symbol} pnl`,
424
+ value: formatSigned(pnl),
425
+ tone: resolveMetricTone(pnl)
426
+ },
427
+ {
428
+ id: `symbol:${symbol}:orders`,
429
+ label: `${symbol} approved`,
430
+ value: String(summary2.approved)
431
+ },
432
+ {
433
+ id: `symbol:${symbol}:winRate`,
434
+ label: `${symbol} win_rate`,
435
+ value: formatRatio(summary2.approvedRisk.winRate)
436
+ }
437
+ ]);
438
+ const details = [
439
+ {
440
+ id: "window",
441
+ label: "window",
442
+ value: `${formatUtcWindowTimestamp(windowStart)} -> ${formatUtcWindowTimestamp(windowEnd)}`
443
+ },
444
+ {
445
+ id: "approved",
446
+ label: "approved",
447
+ value: String(summary.approved)
448
+ },
449
+ {
450
+ id: "confusion",
451
+ label: "TP / FP / TN / FN",
452
+ value: `${summary.truePositive} / ${summary.falsePositive} / ${summary.trueNegative} / ${summary.falseNegative}`
453
+ },
454
+ {
455
+ id: "precisionApproved",
456
+ label: "precision_approved",
457
+ value: formatRatio(summary.precisionApproved)
458
+ },
459
+ {
460
+ id: "recallWinners",
461
+ label: "recall_winners",
462
+ value: formatRatio(summary.recallWinners)
463
+ },
464
+ {
465
+ id: "avgProfitAll",
466
+ label: "avg_profit_all",
467
+ value: formatSigned(summary.avgProfitAll),
468
+ tone: resolveMetricTone(summary.avgProfitAll)
469
+ },
470
+ {
471
+ id: "avgProfitApproved",
472
+ label: "avg_profit_approved",
473
+ value: formatSigned(summary.avgProfitApproved),
474
+ tone: resolveMetricTone(summary.avgProfitApproved)
475
+ },
476
+ {
477
+ id: "avgProfitApprovedPerDay",
478
+ label: "avg_profit_approved_per_day",
479
+ value: formatSigned(summary.avgProfitApprovedPerDay),
480
+ tone: resolveMetricTone(summary.avgProfitApprovedPerDay)
481
+ },
482
+ {
483
+ id: "avgProfitApprovedPerMonth",
484
+ label: "avg_profit_approved_per_month",
485
+ value: formatSigned(summary.avgProfitApprovedPerMonth),
486
+ tone: resolveMetricTone(summary.avgProfitApprovedPerMonth)
487
+ },
488
+ {
489
+ id: "avgApprovedTradesPerDay",
490
+ label: "avg_approved_trades_per_day",
491
+ value: formatNumber(summary.avgApprovedTradesPerDay)
492
+ },
493
+ {
494
+ id: "avgApprovedTradesPerWeek",
495
+ label: "avg_approved_trades_per_week",
496
+ value: formatNumber(summary.avgApprovedTradesPerWeek)
497
+ },
498
+ {
499
+ id: "expectancyDelta",
500
+ label: "expectancy_delta",
501
+ value: formatSigned(summary.expectancyDelta),
502
+ tone: resolveMetricTone(summary.expectancyDelta)
503
+ },
504
+ {
505
+ id: "maxLossStreak",
506
+ label: "max_loss_streak",
507
+ value: String(summary.approvedRisk.maxConsecutiveLosses),
508
+ tone: summary.approvedRisk.maxConsecutiveLosses > 0 ? "warning" : "success"
509
+ },
510
+ ...directionDetails,
511
+ ...symbolDetails
512
+ ];
513
+ return details;
514
+ };
515
+ var buildStrategyWideEquityCurve = (evaluations) => {
516
+ if (!evaluations.length) {
517
+ return [];
518
+ }
519
+ const sorted = [...evaluations].sort(
520
+ (left, right) => (left.timestamp ?? 0) - (right.timestamp ?? 0) || left.symbol.localeCompare(right.symbol) || left.signalId.localeCompare(right.signalId)
521
+ );
522
+ let amount = 100;
523
+ const firstTimestamp = sorted[0]?.timestamp ?? Date.now();
524
+ const orderLog = [[firstTimestamp, amount]];
525
+ for (const evaluation of sorted) {
526
+ amount += evaluation.profit;
527
+ orderLog.push([
528
+ evaluation.timestamp ?? firstTimestamp,
529
+ Number(amount.toFixed(4))
530
+ ]);
531
+ }
532
+ if (orderLog.length === 1) {
533
+ orderLog.push([firstTimestamp, amount]);
534
+ }
535
+ return orderLog;
536
+ };
537
+ var sortAiChartEvaluations = (evaluations) => [...evaluations].sort(
538
+ (left, right) => (left.timestamp ?? 0) - (right.timestamp ?? 0) || left.symbol.localeCompare(right.symbol) || left.signalId.localeCompare(right.signalId)
539
+ );
540
+ var toFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
541
+ var buildStrategyWideOrders = (evaluations) => {
542
+ let amount = 100;
543
+ return sortAiChartEvaluations(evaluations).flatMap((evaluation, index) => {
544
+ const tradeResult = evaluation.tradeResult;
545
+ const equityBefore = amount;
546
+ amount = Number((amount + evaluation.profit).toFixed(4));
547
+ if (!tradeResult) {
548
+ return [];
549
+ }
550
+ const entryPrice = toFiniteNumber(tradeResult?.entryPrice);
551
+ const qty = toFiniteNumber(tradeResult?.qty);
552
+ const notional = entryPrice != null && qty != null ? Number((entryPrice * qty).toFixed(8)) : null;
553
+ return {
554
+ id: `${evaluation.signalId}:${evaluation.timestamp ?? index}`,
555
+ symbol: evaluation.symbol,
556
+ direction: tradeResult?.direction ?? evaluation.direction ?? null,
557
+ timestamp: evaluation.timestamp ?? tradeResult?.exitTimestamp ?? null,
558
+ entryTimestamp: tradeResult?.entryTimestamp ?? evaluation.timestamp ?? null,
559
+ exitTimestamp: tradeResult?.exitTimestamp ?? evaluation.timestamp ?? null,
560
+ exitReason: tradeResult?.exitReason ?? null,
561
+ pnl: evaluation.profit,
562
+ equityBefore,
563
+ equityAfter: amount,
564
+ qty,
565
+ notional,
566
+ requestedEntryPrice: toFiniteNumber(tradeResult?.requestedEntryPrice),
567
+ entryPrice,
568
+ requestedExitPrice: toFiniteNumber(tradeResult?.requestedExitPrice),
569
+ exitPrice: toFiniteNumber(tradeResult?.exitPrice),
570
+ openFee: toFiniteNumber(tradeResult?.openFee),
571
+ closeFee: toFiniteNumber(tradeResult?.closeFee),
572
+ fundingFee: toFiniteNumber(tradeResult?.fundingFee),
573
+ totalFee: toFiniteNumber(tradeResult?.totalFee),
574
+ entrySlippageBps: toFiniteNumber(tradeResult?.entrySlippageBps),
575
+ entryBaseSlippageBps: toFiniteNumber(tradeResult?.entryBaseSlippageBps),
576
+ entrySpreadBps: toFiniteNumber(tradeResult?.entrySpreadBps),
577
+ entrySpreadSlippageBps: toFiniteNumber(
578
+ tradeResult?.entrySpreadSlippageBps
579
+ ),
580
+ entryMarketImpactBps: toFiniteNumber(tradeResult?.entryMarketImpactBps),
581
+ entryDelayRiskBps: toFiniteNumber(tradeResult?.entryDelayRiskBps),
582
+ exitSlippageBps: toFiniteNumber(tradeResult?.exitSlippageBps),
583
+ exitBaseSlippageBps: toFiniteNumber(tradeResult?.exitBaseSlippageBps),
584
+ exitSpreadBps: toFiniteNumber(tradeResult?.exitSpreadBps),
585
+ exitSpreadSlippageBps: toFiniteNumber(tradeResult?.exitSpreadSlippageBps),
586
+ exitMarketImpactBps: toFiniteNumber(tradeResult?.exitMarketImpactBps),
587
+ exitDelayRiskBps: toFiniteNumber(tradeResult?.exitDelayRiskBps),
588
+ totalSlippageCost: toFiniteNumber(tradeResult?.totalSlippageCost),
589
+ sequence: evaluation.sequence ?? index + 1
590
+ };
591
+ });
592
+ };
593
+ var calculateMaxDrawdownPercent = (orderLog) => {
594
+ if (!orderLog.length) {
595
+ return null;
596
+ }
597
+ let peak = orderLog[0]?.[1] ?? 0;
598
+ let maxDrawdownPercent = 0;
599
+ for (const [, amount] of orderLog) {
600
+ if (!Number.isFinite(amount)) {
601
+ continue;
602
+ }
603
+ peak = Math.max(peak, amount);
604
+ if (peak <= 0) {
605
+ continue;
606
+ }
607
+ const drawdownPercent = (peak - amount) / peak * 100;
608
+ maxDrawdownPercent = Math.max(maxDrawdownPercent, drawdownPercent);
609
+ }
610
+ return Number(maxDrawdownPercent.toFixed(1));
611
+ };
612
+ var resolveAiVariantGroup = (params) => {
613
+ const normalizedConfigId = params.configId.trim();
614
+ if (!normalizedConfigId) {
615
+ return null;
616
+ }
617
+ return {
618
+ key: normalizedConfigId,
619
+ label: `config ${normalizedConfigId}`
620
+ };
621
+ };
622
+ var buildAiChartSnapshot = (params) => {
623
+ const {
624
+ evaluatedRows,
625
+ strategyName,
626
+ generatedAt,
627
+ runLabel,
628
+ minQuality,
629
+ datasetId
630
+ } = params;
631
+ const groupsByVariant = /* @__PURE__ */ new Map();
632
+ for (const evaluation of evaluatedRows) {
633
+ const variantGroup = resolveAiVariantGroup({
634
+ configId: evaluation.configId
635
+ });
636
+ if (!variantGroup) {
637
+ continue;
638
+ }
639
+ const existingGroup = groupsByVariant.get(variantGroup.key);
640
+ if (existingGroup) {
641
+ existingGroup.rows.push(evaluation);
642
+ continue;
643
+ }
644
+ groupsByVariant.set(variantGroup.key, {
645
+ key: variantGroup.key,
646
+ label: variantGroup.label,
647
+ rows: [evaluation]
648
+ });
649
+ }
650
+ const variantGroups = [...groupsByVariant.values()].sort(
651
+ (left, right) => left.label.localeCompare(right.label)
652
+ );
653
+ const groupByVariant = variantGroups.length > 1;
654
+ const groups = groupByVariant ? variantGroups : [
655
+ {
656
+ key: strategyName,
657
+ label: "",
658
+ rows: evaluatedRows
659
+ }
660
+ ];
661
+ const cards = [];
662
+ for (const threshold of [minQuality]) {
663
+ for (const group of groups) {
664
+ const thresholdEvaluations = group.rows.map((evaluation) => ({
665
+ ...evaluation,
666
+ aiApproved: evaluation.modelDirection === evaluation.direction && evaluation.quality != null && evaluation.quality >= threshold
667
+ }));
668
+ const summary = summarizeAiTrainEvaluations(thresholdEvaluations);
669
+ const approvedRows = thresholdEvaluations.filter(
670
+ (evaluation) => evaluation.aiApproved
671
+ );
672
+ const orderLog = buildStrategyWideEquityCurve(approvedRows);
673
+ const orders = buildStrategyWideOrders(approvedRows);
674
+ cards.push({
675
+ cardId: `${strategyName}-${group.key}-q${threshold}-${generatedAt}`,
676
+ generatedAt,
677
+ strategyName,
678
+ title: groupByVariant ? `${strategyName} \xB7 ${group.label}` : strategyName,
679
+ subtitle: runLabel ? `q${threshold}+ \xB7 ${runLabel}` : `q${threshold}+`,
680
+ datasetId,
681
+ symbols: [
682
+ ...new Set(
683
+ group.rows.map((evaluation) => evaluation.symbol).filter(Boolean)
684
+ )
685
+ ].sort(),
686
+ orderLog,
687
+ orders,
688
+ stat: null,
689
+ metrics: buildAiChartMetrics({
690
+ summary,
691
+ maxDrawdownPercent: calculateMaxDrawdownPercent(orderLog)
692
+ }),
693
+ details: buildAiChartDetails({
694
+ summary,
695
+ rows: thresholdEvaluations
696
+ }),
697
+ tags: groupByVariant ? [`q${threshold}+`, group.label] : [`q${threshold}+`]
698
+ });
699
+ }
700
+ }
701
+ return {
702
+ mode: "ai",
703
+ generatedAt,
704
+ runLabel,
705
+ strategies: cards
706
+ };
707
+ };
708
+ // Annotate the CommonJS export names for ESM import in node:
709
+ 0 && (module.exports = {
710
+ buildAiChartSnapshot
711
+ });