@tradejs/core 1.0.0

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 (68) hide show
  1. package/README.md +60 -0
  2. package/dist/api.d.mts +7 -0
  3. package/dist/api.d.ts +7 -0
  4. package/dist/api.js +64 -0
  5. package/dist/api.mjs +39 -0
  6. package/dist/async.d.mts +4 -0
  7. package/dist/async.d.ts +4 -0
  8. package/dist/async.js +48 -0
  9. package/dist/async.mjs +20 -0
  10. package/dist/backtest.d.mts +45 -0
  11. package/dist/backtest.d.ts +45 -0
  12. package/dist/backtest.js +574 -0
  13. package/dist/backtest.mjs +355 -0
  14. package/dist/chunk-AYC2QVKI.mjs +35 -0
  15. package/dist/chunk-JG2QPVAV.mjs +190 -0
  16. package/dist/chunk-LIGD3WWX.mjs +1545 -0
  17. package/dist/chunk-M7QGVZ3J.mjs +61 -0
  18. package/dist/chunk-NQ7D3T4E.mjs +10 -0
  19. package/dist/chunk-PXLXXXLA.mjs +67 -0
  20. package/dist/config.d.mts +14 -0
  21. package/dist/config.d.ts +14 -0
  22. package/dist/config.js +49 -0
  23. package/dist/config.mjs +21 -0
  24. package/dist/constants.d.mts +41 -0
  25. package/dist/constants.d.ts +41 -0
  26. package/dist/constants.js +238 -0
  27. package/dist/constants.mjs +50 -0
  28. package/dist/data.d.mts +9 -0
  29. package/dist/data.d.ts +9 -0
  30. package/dist/data.js +100 -0
  31. package/dist/data.mjs +12 -0
  32. package/dist/figures.d.mts +103 -0
  33. package/dist/figures.d.ts +103 -0
  34. package/dist/figures.js +274 -0
  35. package/dist/figures.mjs +239 -0
  36. package/dist/indicators-x3xKl3_W.d.mts +90 -0
  37. package/dist/indicators-x3xKl3_W.d.ts +90 -0
  38. package/dist/indicators.d.mts +124 -0
  39. package/dist/indicators.d.ts +124 -0
  40. package/dist/indicators.js +1631 -0
  41. package/dist/indicators.mjs +66 -0
  42. package/dist/json.d.mts +3 -0
  43. package/dist/json.d.ts +3 -0
  44. package/dist/json.js +34 -0
  45. package/dist/json.mjs +7 -0
  46. package/dist/math.d.mts +35 -0
  47. package/dist/math.d.ts +35 -0
  48. package/dist/math.js +98 -0
  49. package/dist/math.mjs +38 -0
  50. package/dist/pine.d.mts +29 -0
  51. package/dist/pine.d.ts +29 -0
  52. package/dist/pine.js +59 -0
  53. package/dist/pine.mjs +29 -0
  54. package/dist/strategies.d.mts +104 -0
  55. package/dist/strategies.d.ts +104 -0
  56. package/dist/strategies.js +1080 -0
  57. package/dist/strategies.mjs +390 -0
  58. package/dist/tickers.d.mts +7 -0
  59. package/dist/tickers.d.ts +7 -0
  60. package/dist/tickers.js +166 -0
  61. package/dist/tickers.mjs +125 -0
  62. package/dist/time-DEyFa2vI.d.mts +11 -0
  63. package/dist/time-DEyFa2vI.d.ts +11 -0
  64. package/dist/time.d.mts +2 -0
  65. package/dist/time.d.ts +2 -0
  66. package/dist/time.js +58 -0
  67. package/dist/time.mjs +15 -0
  68. package/package.json +99 -0
@@ -0,0 +1,574 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/backtest.ts
31
+ var backtest_exports = {};
32
+ __export(backtest_exports, {
33
+ calculateMaxDrawdown: () => calculateMaxDrawdown,
34
+ calculateStatsFull: () => calculateStatsFull,
35
+ classifyMetric: () => classifyMetric,
36
+ compactOrderLog: () => compactOrderLog,
37
+ createTestSuite: () => createTestSuite,
38
+ generateName: () => generateName,
39
+ generateParamGrid: () => generateParamGrid,
40
+ getBacktestScore: () => getBacktestScore,
41
+ getFormatted: () => getFormatted,
42
+ getTimeline: () => getTimeline,
43
+ mergeConfigs: () => mergeConfigs,
44
+ parseTestName: () => parseTestName,
45
+ sortBestTests: () => sortBestTests
46
+ });
47
+ module.exports = __toCommonJS(backtest_exports);
48
+
49
+ // src/utils/grid.ts
50
+ var import_lodash = __toESM(require("lodash"));
51
+
52
+ // src/constants/index.ts
53
+ var BACKTEST_PRELOAD_DAYS = 160;
54
+ var TestThresholdsConfig = {
55
+ // Период и частота — используем как требования к качеству теста, в скоринг не влияют
56
+ periodDays: {
57
+ thresholds: [30, 120],
58
+ direction: "higher",
59
+ precision: 0
60
+ },
61
+ periodMonths: {
62
+ thresholds: [1, 6],
63
+ direction: "higher",
64
+ precision: 2
65
+ },
66
+ orders: {
67
+ thresholds: [30, 200],
68
+ direction: "higher",
69
+ precision: 0
70
+ },
71
+ wins: {
72
+ thresholds: [30, 100],
73
+ direction: "higher",
74
+ precision: 0
75
+ },
76
+ losses: {
77
+ thresholds: [20, 50],
78
+ direction: "lower",
79
+ precision: 0
80
+ },
81
+ ordersPerMonth: {
82
+ thresholds: [4, 20],
83
+ direction: "higher",
84
+ precision: 2
85
+ },
86
+ exposure: {
87
+ thresholds: [20, 60],
88
+ direction: "higher",
89
+ isPercent: true,
90
+ precision: 1
91
+ },
92
+ // Доходность
93
+ amount: {
94
+ thresholds: [105, 120],
95
+ direction: "higher",
96
+ isAmount: true,
97
+ precision: 2
98
+ },
99
+ maxAmount: {
100
+ thresholds: [140, 180],
101
+ direction: "higher",
102
+ isAmount: true,
103
+ precision: 2
104
+ },
105
+ minAmount: {
106
+ thresholds: [80, 90],
107
+ direction: "higher",
108
+ isAmount: true,
109
+ precision: 2
110
+ },
111
+ netProfit: {
112
+ thresholds: [5, 20],
113
+ direction: "higher",
114
+ isAmount: true,
115
+ precision: 2
116
+ },
117
+ totalReturn: {
118
+ thresholds: [10, 50],
119
+ direction: "higher",
120
+ isPercent: true,
121
+ precision: 1
122
+ },
123
+ cagr: {
124
+ thresholds: [15, 40],
125
+ direction: "higher",
126
+ isPercent: true,
127
+ precision: 1
128
+ },
129
+ // Риск и риск/доходность
130
+ maxDrawdown: {
131
+ thresholds: [25, 12],
132
+ direction: "lower",
133
+ isPercent: true,
134
+ precision: 1
135
+ },
136
+ calmar: {
137
+ thresholds: [0.5, 2],
138
+ direction: "higher",
139
+ precision: 2
140
+ },
141
+ // Качество сделок
142
+ winRate: {
143
+ thresholds: [40, 60],
144
+ direction: "higher",
145
+ isPercent: true,
146
+ precision: 1
147
+ },
148
+ riskRewardRatio: {
149
+ thresholds: [1.5, 2.5],
150
+ direction: "higher",
151
+ precision: 2
152
+ },
153
+ expectancy: {
154
+ thresholds: [0.3, 1],
155
+ direction: "higher",
156
+ isPercent: true,
157
+ precision: 2
158
+ },
159
+ maxConsecutiveWins: {
160
+ thresholds: [2, 6],
161
+ direction: "higher",
162
+ precision: 0
163
+ },
164
+ maxConsecutiveLosses: {
165
+ thresholds: [5, 2],
166
+ direction: "lower",
167
+ precision: 0
168
+ },
169
+ // Sharpe (годовой, по месячным ретернам equity)
170
+ sharpeRatio: {
171
+ thresholds: [0.5, 1.5],
172
+ direction: "higher",
173
+ precision: 2
174
+ },
175
+ score: {
176
+ thresholds: [10, 100],
177
+ direction: "higher",
178
+ precision: 0
179
+ }
180
+ };
181
+
182
+ // src/utils/timestamp.ts
183
+ var import_date_fns = require("date-fns");
184
+ var import_date_fns2 = require("date-fns");
185
+ var TIMELINE_STEP = 864e5;
186
+ var getTimestamp = (days = 0) => {
187
+ if (days > 0) {
188
+ return (0, import_date_fns2.getUnixTime)((0, import_date_fns2.subDays)(/* @__PURE__ */ new Date(), days)) * 1e3;
189
+ }
190
+ return (0, import_date_fns2.getUnixTime)(/* @__PURE__ */ new Date()) * 1e3;
191
+ };
192
+ var getTimeline = (start = getTimestamp(BACKTEST_PRELOAD_DAYS), end = getTimestamp(), step = TIMELINE_STEP) => {
193
+ const res = new Array();
194
+ for (let ind = start; ind <= end; ind += step) {
195
+ res.push(ind);
196
+ }
197
+ return res;
198
+ };
199
+ var compactOrderLog = (timeline, orderLog) => {
200
+ const result = [];
201
+ let currentAmount = orderLog.length > 0 && orderLog[0].amount != null ? orderLog[0].amount : 100;
202
+ let orderLogCursor = 0;
203
+ for (let timelineIndex = 0; timelineIndex < timeline.length; timelineIndex++) {
204
+ const currentTimestamp = timeline[timelineIndex];
205
+ let lastApplicableOrderIndex = -1;
206
+ let nextCursor = orderLogCursor;
207
+ for (let checkIndex = orderLogCursor; checkIndex < orderLog.length; checkIndex++) {
208
+ const checkOrder = orderLog[checkIndex];
209
+ if (checkOrder.timestamp <= currentTimestamp) {
210
+ lastApplicableOrderIndex = checkIndex;
211
+ nextCursor = checkIndex + 1;
212
+ } else {
213
+ break;
214
+ }
215
+ }
216
+ if (lastApplicableOrderIndex !== -1) {
217
+ currentAmount = orderLog[lastApplicableOrderIndex].amount;
218
+ orderLogCursor = nextCursor;
219
+ }
220
+ result.push([currentTimestamp, currentAmount]);
221
+ }
222
+ return result;
223
+ };
224
+
225
+ // src/utils/uuid.ts
226
+ var import_uuid = require("uuid");
227
+ var uuid = (len = 12) => {
228
+ const uuid2 = (0, import_uuid.v4)();
229
+ return uuid2.slice(-len);
230
+ };
231
+
232
+ // src/utils/grid.ts
233
+ var generateParamGrid = (paramOptions) => {
234
+ const keys = Object.keys(paramOptions);
235
+ const combinations = [];
236
+ const helper = (index = 0, current = {}) => {
237
+ if (index === keys.length) {
238
+ combinations.push(current);
239
+ return;
240
+ }
241
+ const key = keys[index];
242
+ for (const value of paramOptions[key] || []) {
243
+ const copiedValue = typeof value === "object" && value !== null ? structuredClone(value) : value;
244
+ helper(index + 1, {
245
+ ...current,
246
+ [key]: copiedValue
247
+ });
248
+ }
249
+ };
250
+ helper();
251
+ return combinations;
252
+ };
253
+ var generateName = (prefix) => `${prefix}_${uuid(6)}`;
254
+ var mergeConfigs = (configs) => {
255
+ const result = {};
256
+ for (const config of configs) {
257
+ for (const [key, value] of Object.entries(config)) {
258
+ if (!result[key]) {
259
+ result[key] = [];
260
+ }
261
+ const clonedValue = typeof value === "object" && value !== null ? import_lodash.default.cloneDeep(value) : value;
262
+ const isDuplicate = result[key].some(
263
+ (existing) => import_lodash.default.isEqual(existing, value)
264
+ );
265
+ if (!isDuplicate) {
266
+ result[key].push(clonedValue);
267
+ }
268
+ }
269
+ }
270
+ for (const key in result) {
271
+ if (result[key].every((v) => typeof v === "number")) {
272
+ result[key] = import_lodash.default.sortBy(result[key]);
273
+ }
274
+ }
275
+ return result;
276
+ };
277
+ var createTestSuite = (userName, tickers, strategyName, backtestConfig, connectorName) => {
278
+ const start = getTimestamp(BACKTEST_PRELOAD_DAYS);
279
+ const end = getTimestamp();
280
+ const testSuiteId = uuid(6);
281
+ const paramGrid = generateParamGrid(backtestConfig);
282
+ return tickers.flatMap(
283
+ (symbol) => paramGrid.map((params) => {
284
+ const testId = uuid(6);
285
+ return {
286
+ userName,
287
+ name: `${symbol}_${testSuiteId}_${testId}`,
288
+ testId,
289
+ testSuiteId,
290
+ symbol,
291
+ options: { start, end },
292
+ strategyName,
293
+ strategyConfig: params,
294
+ connectorName
295
+ };
296
+ })
297
+ );
298
+ };
299
+
300
+ // src/utils/tests.ts
301
+ var parseTestName = (testName) => {
302
+ const [symbol, testSuiteId, testId] = testName.split("_");
303
+ return { symbol, testSuiteId, testId };
304
+ };
305
+
306
+ // src/utils/stat.ts
307
+ var import_date_fns3 = require("date-fns");
308
+
309
+ // src/utils/math.ts
310
+ var import_lodash2 = __toESM(require("lodash"));
311
+ var round = (value, precision = 2) => precision > 0 ? Math.round(value * 10 ** precision) / 10 ** precision : Math.round(value);
312
+ var sum = (xs) => xs.reduce((a, b) => a + b, 0);
313
+ var mean = (xs) => xs.length ? sum(xs) / xs.length : 0;
314
+ var absReturns = (data) => data.map((p) => p.close.amount - p.open.amount);
315
+ var relReturns = (data) => data.map((p) => (p.close.amount - p.open.amount) / p.open.amount);
316
+ var equityPoints = (data) => data.flatMap((p) => [
317
+ { ts: p.open.timestamp, amount: p.open.amount },
318
+ { ts: p.close.timestamp, amount: p.close.amount }
319
+ ]).sort((a, b) => a.ts - b.ts);
320
+
321
+ // src/utils/stat.ts
322
+ var calcStreaks = (retsAbs) => {
323
+ let maxW = 0, maxL = 0, cw = 0, cl = 0;
324
+ for (const r of retsAbs) {
325
+ if (r > 0) {
326
+ cw++;
327
+ cl = 0;
328
+ if (cw > maxW) maxW = cw;
329
+ } else if (r < 0) {
330
+ cl++;
331
+ cw = 0;
332
+ if (cl > maxL) maxL = cl;
333
+ } else {
334
+ cw = 0;
335
+ cl = 0;
336
+ }
337
+ }
338
+ return { maxConsecutiveWins: maxW, maxConsecutiveLosses: maxL };
339
+ };
340
+ var calculateMaxDrawdown = (amounts) => {
341
+ let max = amounts[0];
342
+ let maxDrawdown = 0;
343
+ for (const amount of amounts) {
344
+ if (amount > max) {
345
+ max = amount;
346
+ }
347
+ const drawdown = (max - amount) / max * 100;
348
+ if (drawdown > maxDrawdown) {
349
+ maxDrawdown = drawdown;
350
+ }
351
+ }
352
+ return maxDrawdown;
353
+ };
354
+ var computeMonthlyEquityStats = (positionLogData, opts) => {
355
+ const MAR = opts?.mar ?? 0;
356
+ const useSample = !!opts?.sampleStd;
357
+ const tsMul = (opts?.tsUnit ?? "ms") === "s" ? 1e3 : 1;
358
+ if (!positionLogData.length) {
359
+ return {
360
+ eomSeries: [],
361
+ monthlyReturns: [],
362
+ monthlyMean: 0,
363
+ monthlyStd: 0,
364
+ monthlyDownsideStd: 0,
365
+ sharpeMonthly: null,
366
+ sharpeMonthlyAnnualized: null,
367
+ sortinoMonthly: null,
368
+ sortinoMonthlyAnnualized: null,
369
+ positiveMonths: 0,
370
+ maxMonthlyGain: 0,
371
+ maxMonthlyDrop: 0
372
+ };
373
+ }
374
+ const equityPoints2 = positionLogData.flatMap((p) => [
375
+ { ts: p.open.timestamp * tsMul, amount: p.open.amount },
376
+ { ts: p.close.timestamp * tsMul, amount: p.close.amount }
377
+ ]).sort((a, b) => a.ts - b.ts);
378
+ const startTs = equityPoints2[0].ts;
379
+ const endTs = equityPoints2[equityPoints2.length - 1].ts;
380
+ const eomSeries = [];
381
+ let monthCursor = (0, import_date_fns3.startOfMonth)(new Date(startTs));
382
+ const lastMonth = (0, import_date_fns3.endOfMonth)(new Date(endTs));
383
+ let i = 0;
384
+ let lastAmount = equityPoints2[0].amount;
385
+ while (monthCursor <= lastMonth) {
386
+ const eom = (0, import_date_fns3.endOfMonth)(monthCursor);
387
+ const eomTs = eom.getTime();
388
+ while (i < equityPoints2.length && equityPoints2[i].ts <= eomTs) {
389
+ lastAmount = equityPoints2[i].amount;
390
+ i += 1;
391
+ }
392
+ const key = `${eom.getFullYear()}-${String(eom.getMonth() + 1).padStart(2, "0")}`;
393
+ eomSeries.push({ month: key, ts: eomTs, amount: lastAmount });
394
+ monthCursor = (0, import_date_fns3.addMonths)(monthCursor, 1);
395
+ }
396
+ const monthlyReturns = [];
397
+ for (let k = 1; k < eomSeries.length; k++) {
398
+ const prev = eomSeries[k - 1].amount;
399
+ const curr = eomSeries[k].amount;
400
+ monthlyReturns.push(prev > 0 ? curr / prev - 1 : 0);
401
+ }
402
+ const n = monthlyReturns.length;
403
+ const monthlyMean = n ? monthlyReturns.reduce((a, b) => a + b, 0) / n : 0;
404
+ const variance = n ? monthlyReturns.reduce((a, v) => a + (v - monthlyMean) ** 2, 0) / (useSample && n > 1 ? n - 1 : n) : 0;
405
+ const monthlyStd = Math.sqrt(variance);
406
+ const downside = monthlyReturns.map((r) => Math.min(r - MAR, 0)).filter((v) => v < 0);
407
+ const nd = downside.length;
408
+ const downsideVar = nd ? downside.reduce((a, v) => a + v * v, 0) / (useSample && nd > 1 ? nd - 1 : nd) : 0;
409
+ const monthlyDownsideStd = Math.sqrt(downsideVar);
410
+ const sharpeMonthly = monthlyStd > 0 ? (monthlyMean - MAR) / monthlyStd : null;
411
+ const sortinoMonthly = monthlyDownsideStd > 0 ? (monthlyMean - MAR) / monthlyDownsideStd : null;
412
+ const sharpeMonthlyAnnualized = sharpeMonthly === null ? null : sharpeMonthly * Math.sqrt(12);
413
+ const sortinoMonthlyAnnualized = sortinoMonthly === null ? null : sortinoMonthly * Math.sqrt(12);
414
+ const positiveMonths = monthlyReturns.filter((r) => r > 0).length;
415
+ const maxMonthlyGain = n ? Math.max(...monthlyReturns) : 0;
416
+ const maxMonthlyDrop = n ? Math.min(...monthlyReturns) : 0;
417
+ return {
418
+ eomSeries,
419
+ monthlyReturns,
420
+ monthlyMean,
421
+ monthlyStd,
422
+ monthlyDownsideStd,
423
+ sharpeMonthly,
424
+ sharpeMonthlyAnnualized,
425
+ sortinoMonthly,
426
+ sortinoMonthlyAnnualized,
427
+ positiveMonths,
428
+ maxMonthlyGain,
429
+ maxMonthlyDrop
430
+ };
431
+ };
432
+ var calculateStatsFull = (positionLogData) => {
433
+ if (!positionLogData.length) return null;
434
+ const retsAbs = absReturns(positionLogData);
435
+ const retsRel = relReturns(positionLogData);
436
+ const points = equityPoints(positionLogData);
437
+ const startTs = points[0].ts;
438
+ const endTs = points[points.length - 1].ts;
439
+ const periodMs = (0, import_date_fns3.differenceInMilliseconds)(new Date(endTs), new Date(startTs));
440
+ const periodDays = periodMs / (1e3 * 60 * 60 * 24);
441
+ const periodMonths = periodDays / 30.4375;
442
+ const trades = positionLogData.length;
443
+ const tradesPerMonth = periodMonths > 0 ? trades / periodMonths : 0;
444
+ const durations = positionLogData.map(
445
+ (p) => p.close.timestamp - p.open.timestamp
446
+ );
447
+ const totalTime = endTs - startTs;
448
+ const exposure = totalTime > 0 ? sum(durations) / totalTime * 100 : 0;
449
+ const initialAmount = points[0].amount;
450
+ const finalAmount = points[points.length - 1].amount;
451
+ const netProfit = finalAmount - initialAmount;
452
+ const totalReturn = initialAmount > 0 ? (finalAmount / initialAmount - 1) * 100 : 0;
453
+ const cagr = periodMonths > 0 && initialAmount > 0 ? (Math.pow(finalAmount / initialAmount, 12 / periodMonths) - 1) * 100 : 0;
454
+ const allAmounts = points.map((p) => p.amount);
455
+ const maxDrawdown = calculateMaxDrawdown(allAmounts);
456
+ const calmar = maxDrawdown > 0 ? cagr / maxDrawdown : null;
457
+ const wins = retsAbs.filter((x) => x > 0).length;
458
+ const losses = retsAbs.filter((x) => x <= 0).length;
459
+ const winRate = trades ? wins / trades * 100 : 0;
460
+ const avgWinAbs = mean(retsAbs.filter((x) => x > 0));
461
+ const avgLossAbs = Math.abs(mean(retsAbs.filter((x) => x < 0)));
462
+ const payoff = avgLossAbs > 0 ? avgWinAbs / avgLossAbs : null;
463
+ const avgWinRel = mean(retsRel.filter((x) => x > 0));
464
+ const avgLossRel = Math.abs(mean(retsRel.filter((x) => x < 0)));
465
+ const pWin = trades ? wins / trades : 0;
466
+ const expectancyPerTrade = (pWin * avgWinRel - (1 - pWin) * avgLossRel) * 100;
467
+ const { maxConsecutiveWins, maxConsecutiveLosses } = calcStreaks(retsAbs);
468
+ const monthly = computeMonthlyEquityStats(positionLogData, {
469
+ mar: 0,
470
+ // MAR=0, при желании можно параметризовать
471
+ sampleStd: false,
472
+ // population std
473
+ tsUnit: "ms"
474
+ // timestamps в миллисекундах
475
+ });
476
+ const sharpe = (monthly.sharpeMonthly ?? null) !== null ? monthly.sharpeMonthly * Math.sqrt(12) : null;
477
+ const res = {
478
+ // Период и частота
479
+ periodDays: round(periodDays),
480
+ periodMonths: round(periodMonths),
481
+ orders: trades,
482
+ wins,
483
+ losses,
484
+ ordersPerMonth: round(tradesPerMonth),
485
+ exposure: round(exposure),
486
+ // Доходность
487
+ amount: round(finalAmount),
488
+ maxAmount: round(Math.max(...allAmounts)),
489
+ minAmount: round(Math.min(...allAmounts)),
490
+ netProfit: round(netProfit),
491
+ totalReturn: round(totalReturn),
492
+ cagr: round(cagr),
493
+ // Риск и Calmar
494
+ maxDrawdown: round(maxDrawdown),
495
+ calmar: calmar === null ? null : round(calmar),
496
+ // Качество сделок
497
+ winRate: round(winRate),
498
+ riskRewardRatio: payoff === null ? null : round(payoff),
499
+ expectancy: round(expectancyPerTrade),
500
+ maxConsecutiveWins,
501
+ maxConsecutiveLosses,
502
+ // Sharpe (годовой) по месячным ретёрнам equity
503
+ sharpeRatio: sharpe === null ? null : round(sharpe)
504
+ };
505
+ const score = getBacktestScore(res);
506
+ return {
507
+ ...res,
508
+ score
509
+ };
510
+ };
511
+ var classifyMetric = (name, value) => {
512
+ const { thresholds, direction } = TestThresholdsConfig[name];
513
+ if (direction === "higher") {
514
+ if (value >= thresholds[1]) return "success";
515
+ if (value >= thresholds[0]) return "warning";
516
+ return "error";
517
+ } else {
518
+ if (value <= thresholds[1]) return "success";
519
+ if (value <= thresholds[0]) return "warning";
520
+ return "error";
521
+ }
522
+ };
523
+ var getBacktestScore = (stat) => {
524
+ if (!stat) {
525
+ return 0;
526
+ }
527
+ const netProfit = Number(stat.netProfit ?? 0);
528
+ const winRate = Number(stat.winRate ?? 0);
529
+ if (!Number.isFinite(netProfit) || !Number.isFinite(winRate)) {
530
+ return 0;
531
+ }
532
+ return Math.round(netProfit * winRate);
533
+ };
534
+ var sortBestTests = (results, limit = 5) => {
535
+ return results.sort((a, b) => (b.stat.amount ?? 0) - (a.stat.amount ?? 0)).slice(0, limit);
536
+ };
537
+ var getFormatted = (stat, key) => {
538
+ if (!stat) {
539
+ return {
540
+ formatted: "0",
541
+ level: "error"
542
+ };
543
+ }
544
+ const raw = stat[key];
545
+ if (raw == null || typeof raw === "string") {
546
+ return {
547
+ formatted: String(raw ?? "-"),
548
+ level: "error"
549
+ };
550
+ }
551
+ const config = TestThresholdsConfig[key];
552
+ const level = config ? classifyMetric(key, raw) : "success";
553
+ const formatted = config ? `${raw.toFixed(config.precision)}${config.isPercent ? "%" : ""}${config.isAmount ? "$" : ""}` : String(raw);
554
+ return {
555
+ formatted,
556
+ level
557
+ };
558
+ };
559
+ // Annotate the CommonJS export names for ESM import in node:
560
+ 0 && (module.exports = {
561
+ calculateMaxDrawdown,
562
+ calculateStatsFull,
563
+ classifyMetric,
564
+ compactOrderLog,
565
+ createTestSuite,
566
+ generateName,
567
+ generateParamGrid,
568
+ getBacktestScore,
569
+ getFormatted,
570
+ getTimeline,
571
+ mergeConfigs,
572
+ parseTestName,
573
+ sortBestTests
574
+ });