@tradejs/core 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 (41) hide show
  1. package/dist/backtest.d.mts +109 -9
  2. package/dist/backtest.d.ts +109 -9
  3. package/dist/backtest.js +487 -145
  4. package/dist/backtest.mjs +414 -82
  5. package/dist/chunk-7P2KNFD4.mjs +11847 -0
  6. package/dist/{chunk-JLORHLL6.mjs → chunk-DXJ4NCFJ.mjs} +65 -6
  7. package/dist/chunk-OJPHC3S2.mjs +8 -0
  8. package/dist/{chunk-PQETJ42A.mjs → chunk-PNBS6J3G.mjs} +22 -1
  9. package/dist/constants.d.mts +26 -5
  10. package/dist/constants.d.ts +26 -5
  11. package/dist/constants.js +81 -9
  12. package/dist/constants.mjs +31 -5
  13. package/dist/data.mjs +3 -5
  14. package/dist/grid.d.mts +9 -0
  15. package/dist/grid.d.ts +9 -0
  16. package/dist/grid.js +168 -0
  17. package/dist/grid.mjs +98 -0
  18. package/dist/indicators-Da_i06-8.d.mts +288 -0
  19. package/dist/indicators-Da_i06-8.d.ts +288 -0
  20. package/dist/indicators.d.mts +4 -39
  21. package/dist/indicators.d.ts +4 -39
  22. package/dist/indicators.js +10488 -423
  23. package/dist/indicators.mjs +11 -3
  24. package/dist/strategies.d.mts +31 -12
  25. package/dist/strategies.d.ts +31 -12
  26. package/dist/strategies.js +11283 -336
  27. package/dist/strategies.mjs +1246 -119
  28. package/dist/{time-BMkFD4Kd.d.mts → time-BQ3AXmxo.d.mts} +3 -1
  29. package/dist/{time-BMkFD4Kd.d.ts → time-BQ3AXmxo.d.ts} +3 -1
  30. package/dist/time.d.mts +1 -1
  31. package/dist/time.d.ts +1 -1
  32. package/dist/time.js +38 -0
  33. package/dist/time.mjs +6 -2
  34. package/dist/trade.d.mts +54 -0
  35. package/dist/trade.d.ts +54 -0
  36. package/dist/trade.js +352 -0
  37. package/dist/trade.mjs +264 -0
  38. package/package.json +19 -5
  39. package/dist/chunk-622V7IAT.mjs +0 -1810
  40. package/dist/indicators-B-GGjP5F.d.mts +0 -65
  41. package/dist/indicators-B-GGjP5F.d.ts +0 -65
package/dist/backtest.js CHANGED
@@ -30,27 +30,45 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/backtest.ts
31
31
  var backtest_exports = {};
32
32
  __export(backtest_exports, {
33
+ calculateAdvancedTradeMetrics: () => calculateAdvancedTradeMetrics,
33
34
  calculateMaxDrawdown: () => calculateMaxDrawdown,
34
35
  calculateStatsFull: () => calculateStatsFull,
35
36
  classifyMetric: () => classifyMetric,
36
37
  compactOrderLog: () => compactOrderLog,
37
- createTestSuite: () => createTestSuite,
38
- generateName: () => generateName,
39
- generateParamGrid: () => generateParamGrid,
40
38
  getBacktestScore: () => getBacktestScore,
41
39
  getFormatted: () => getFormatted,
42
40
  getTimeline: () => getTimeline,
43
- mergeConfigs: () => mergeConfigs,
44
41
  parseTestName: () => parseTestName,
45
42
  sortBestTests: () => sortBestTests
46
43
  });
47
44
  module.exports = __toCommonJS(backtest_exports);
48
45
 
49
- // src/utils/grid.ts
50
- var import_lodash = __toESM(require("lodash"));
46
+ // src/utils/tests.ts
47
+ var parseTestName = (testName) => {
48
+ const [symbol, testSuiteId, testId] = testName.split("_");
49
+ return { symbol, testSuiteId, testId };
50
+ };
51
+
52
+ // src/utils/stat.ts
53
+ var import_date_fns = require("date-fns");
51
54
 
52
55
  // src/constants/index.ts
56
+ var BACKTEST_EXECUTION_DELAY_MS = 5 * 6e4;
53
57
  var BACKTEST_DEFAULT_DAYS = 160;
58
+ var DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS = [
59
+ "BTCUSDT",
60
+ "ETHUSDT"
61
+ ];
62
+ var DERIVATIVES_CONTEXT_DEFAULT_EXTRA_REFERENCE_SYMBOLS = [
63
+ "BNBUSDT",
64
+ "SOLUSDT",
65
+ "TRXUSDT",
66
+ "XRPUSDT"
67
+ ];
68
+ var DERIVATIVES_CONTEXT_REFERENCE_SYMBOLS = [
69
+ ...DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS,
70
+ ...DERIVATIVES_CONTEXT_DEFAULT_EXTRA_REFERENCE_SYMBOLS
71
+ ];
54
72
  var TestThresholdsConfig = {
55
73
  // Период и частота — используем как требования к качеству теста, в скоринг не влияют
56
74
  periodDays: {
@@ -109,9 +127,10 @@ var TestThresholdsConfig = {
109
127
  precision: 2
110
128
  },
111
129
  netProfit: {
112
- thresholds: [5, 20],
130
+ thresholds: [0, 0],
113
131
  direction: "higher",
114
132
  isAmount: true,
133
+ neutralValue: 0,
115
134
  precision: 2
116
135
  },
117
136
  totalReturn: {
@@ -179,135 +198,8 @@ var TestThresholdsConfig = {
179
198
  }
180
199
  };
181
200
 
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_DEFAULT_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_node_crypto = require("crypto");
227
- var uuid = (len = 12) => {
228
- const uuid2 = (0, import_node_crypto.randomUUID)();
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_DEFAULT_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
201
  // src/utils/math.ts
310
- var import_lodash2 = __toESM(require("lodash"));
202
+ var import_lodash = __toESM(require("lodash"));
311
203
  var round = (value, precision = 2) => precision > 0 ? Math.round(value * 10 ** precision) / 10 ** precision : Math.round(value);
312
204
  var sum = (xs) => xs.reduce((a, b) => a + b, 0);
313
205
  var mean = (xs) => xs.length ? sum(xs) / xs.length : 0;
@@ -351,6 +243,412 @@ var calculateMaxDrawdown = (amounts) => {
351
243
  }
352
244
  return maxDrawdown;
353
245
  };
246
+ var MS_IN_DAY = 24 * 60 * 60 * 1e3;
247
+ var DAYS_IN_YEAR = 365;
248
+ var isFiniteMetric = (value) => typeof value === "number" && Number.isFinite(value);
249
+ var safeRatio = (numerator, denominator) => {
250
+ if (!isFiniteMetric(numerator) || !isFiniteMetric(denominator)) {
251
+ return null;
252
+ }
253
+ if (Math.abs(denominator) <= Number.EPSILON) {
254
+ return null;
255
+ }
256
+ return numerator / denominator;
257
+ };
258
+ var percentile = (values, p) => {
259
+ if (!values.length) {
260
+ return null;
261
+ }
262
+ const sorted = [...values].sort((a, b) => a - b);
263
+ const index = (sorted.length - 1) * p;
264
+ const lower = Math.floor(index);
265
+ const upper = Math.ceil(index);
266
+ if (lower === upper) {
267
+ return sorted[lower];
268
+ }
269
+ const weight = index - lower;
270
+ return sorted[lower] * (1 - weight) + sorted[upper] * weight;
271
+ };
272
+ var medianValue = (values) => percentile(values, 0.5);
273
+ var getMonthKey = (timestamp) => {
274
+ const date = new Date(timestamp);
275
+ return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}`;
276
+ };
277
+ var getQuarterKey = (timestamp) => {
278
+ const date = new Date(timestamp);
279
+ return `${date.getUTCFullYear()} Q${Math.floor(date.getUTCMonth() / 3) + 1}`;
280
+ };
281
+ var resolveTradeSession = (timestamp) => {
282
+ const hour = new Date(timestamp).getUTCHours();
283
+ if (hour < 8) {
284
+ return "Asia";
285
+ }
286
+ if (hour < 16) {
287
+ return "Europe";
288
+ }
289
+ return "US";
290
+ };
291
+ var getDateKey = (timestamp) => {
292
+ const date = new Date(timestamp);
293
+ return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
294
+ };
295
+ var calculateDrawdownStats = (trades, orderLog) => {
296
+ const explicitPoints = (orderLog ?? []).map((point) => ({ timestamp: point[0], amount: point[1] })).filter(
297
+ (point) => isFiniteMetric(point.timestamp) && isFiniteMetric(point.amount)
298
+ ).sort((a, b) => a.timestamp - b.timestamp);
299
+ const points = explicitPoints.length ? explicitPoints : trades.slice().sort((a, b) => a.timestamp - b.timestamp).reduce(
300
+ (acc, trade) => {
301
+ const previous = acc[acc.length - 1]?.amount ?? 0;
302
+ acc.push({
303
+ timestamp: trade.timestamp,
304
+ amount: previous + trade.pnl
305
+ });
306
+ return acc;
307
+ },
308
+ [{ timestamp: trades[0]?.timestamp ?? 0, amount: 0 }]
309
+ );
310
+ if (!points.length) {
311
+ return { absolute: null, percent: null };
312
+ }
313
+ let peak = points[0].amount;
314
+ let maxAbsolute = 0;
315
+ let maxPercent = 0;
316
+ for (const point of points) {
317
+ if (point.amount > peak) {
318
+ peak = point.amount;
319
+ }
320
+ const absolute = peak - point.amount;
321
+ maxAbsolute = Math.max(maxAbsolute, absolute);
322
+ if (peak > 0) {
323
+ maxPercent = Math.max(maxPercent, absolute / peak * 100);
324
+ }
325
+ }
326
+ return { absolute: maxAbsolute, percent: maxPercent };
327
+ };
328
+ var calculateWorstRollingPnl = (trades, days) => {
329
+ if (!trades.length) {
330
+ return null;
331
+ }
332
+ const sorted = trades.slice().sort((a, b) => a.timestamp - b.timestamp);
333
+ const windowMs = days * MS_IN_DAY;
334
+ let start = 0;
335
+ let rollingPnl = 0;
336
+ let worstPnl = 0;
337
+ for (let end = 0; end < sorted.length; end += 1) {
338
+ rollingPnl += sorted[end].pnl;
339
+ while (start <= end && sorted[end].timestamp - sorted[start].timestamp > windowMs) {
340
+ rollingPnl -= sorted[start].pnl;
341
+ start += 1;
342
+ }
343
+ worstPnl = Math.min(worstPnl, rollingPnl);
344
+ }
345
+ return worstPnl;
346
+ };
347
+ var calculateLossStreak = (trades) => {
348
+ let current = 0;
349
+ let max = 0;
350
+ for (const trade of trades.slice().sort((a, b) => a.timestamp - b.timestamp)) {
351
+ if (trade.pnl < 0) {
352
+ current += 1;
353
+ max = Math.max(max, current);
354
+ continue;
355
+ }
356
+ current = 0;
357
+ }
358
+ return max;
359
+ };
360
+ var normalizeExitReason = (reason) => {
361
+ const normalized = String(reason ?? "").trim().toLowerCase();
362
+ if (normalized === "tp" || normalized === "take_profit") {
363
+ return "takeProfit";
364
+ }
365
+ if (normalized === "sl" || normalized === "stop_loss") {
366
+ return "stopLoss";
367
+ }
368
+ if (normalized === "exit" || normalized === "close" || normalized === "closed") {
369
+ return "exit";
370
+ }
371
+ return "unknown";
372
+ };
373
+ var calculateExitBreakdown = (trades) => {
374
+ const counts = {
375
+ takeProfit: 0,
376
+ stopLoss: 0,
377
+ exit: 0,
378
+ unknown: 0
379
+ };
380
+ for (const trade of trades) {
381
+ counts[normalizeExitReason(trade.exitReason)] += 1;
382
+ }
383
+ const total = trades.length;
384
+ const bucket = (count) => ({
385
+ count,
386
+ share: total ? count / total * 100 : null
387
+ });
388
+ return {
389
+ takeProfit: bucket(counts.takeProfit),
390
+ stopLoss: bucket(counts.stopLoss),
391
+ exit: bucket(counts.exit),
392
+ unknown: bucket(counts.unknown)
393
+ };
394
+ };
395
+ var calculateDailyPnlSeries = (trades, startTimestamp, endTimestamp) => {
396
+ const approvedTrades = trades.filter((trade) => trade.approved !== false);
397
+ if (!approvedTrades.length) {
398
+ return [];
399
+ }
400
+ const firstTimestamp = startTimestamp ?? Math.min(...approvedTrades.map((trade) => trade.timestamp));
401
+ const lastTimestamp = endTimestamp ?? Math.max(...approvedTrades.map((trade) => trade.timestamp));
402
+ if (!isFiniteMetric(firstTimestamp) || !isFiniteMetric(lastTimestamp) || lastTimestamp < firstTimestamp) {
403
+ return [];
404
+ }
405
+ const startDate = Date.UTC(
406
+ new Date(firstTimestamp).getUTCFullYear(),
407
+ new Date(firstTimestamp).getUTCMonth(),
408
+ new Date(firstTimestamp).getUTCDate()
409
+ );
410
+ const endDate = Date.UTC(
411
+ new Date(lastTimestamp).getUTCFullYear(),
412
+ new Date(lastTimestamp).getUTCMonth(),
413
+ new Date(lastTimestamp).getUTCDate()
414
+ );
415
+ const daily = /* @__PURE__ */ new Map();
416
+ for (let ts = startDate; ts <= endDate; ts += MS_IN_DAY) {
417
+ daily.set(getDateKey(ts), 0);
418
+ }
419
+ for (const trade of approvedTrades) {
420
+ const key = getDateKey(trade.timestamp);
421
+ daily.set(key, (daily.get(key) ?? 0) + trade.pnl);
422
+ }
423
+ return [...daily.values()];
424
+ };
425
+ var calculateStd = (values, valueMean) => {
426
+ if (!values.length) {
427
+ return 0;
428
+ }
429
+ return Math.sqrt(
430
+ values.reduce((acc, value) => acc + (value - valueMean) ** 2, 0) / values.length
431
+ );
432
+ };
433
+ var calculateSkewness = (values) => {
434
+ if (values.length < 3) {
435
+ return null;
436
+ }
437
+ const valueMean = mean(values);
438
+ const std = calculateStd(values, valueMean);
439
+ if (std <= Number.EPSILON) {
440
+ return null;
441
+ }
442
+ return values.reduce((acc, value) => acc + ((value - valueMean) / std) ** 3, 0) / values.length;
443
+ };
444
+ var sumTopPositiveProfitShare = (pnls, count) => {
445
+ const grossProfit = pnls.filter((pnl) => pnl > 0).reduce((acc, pnl) => acc + pnl, 0);
446
+ if (grossProfit <= 0) {
447
+ return null;
448
+ }
449
+ const topProfit = pnls.filter((pnl) => pnl > 0).sort((a, b) => b - a).slice(0, count).reduce((acc, pnl) => acc + pnl, 0);
450
+ return topProfit / grossProfit * 100;
451
+ };
452
+ var concentrationPercent = (items, limit) => {
453
+ const totals = /* @__PURE__ */ new Map();
454
+ for (const item of items) {
455
+ totals.set(item.key, (totals.get(item.key) ?? 0) + Math.abs(item.pnl));
456
+ }
457
+ const totalAbsPnl = [...totals.values()].reduce(
458
+ (acc, value) => acc + value,
459
+ 0
460
+ );
461
+ if (totalAbsPnl <= 0) {
462
+ return null;
463
+ }
464
+ const topAbsPnl = [...totals.values()].sort((a, b) => b - a).slice(0, limit).reduce((acc, value) => acc + value, 0);
465
+ return topAbsPnl / totalAbsPnl * 100;
466
+ };
467
+ var calculateAdvancedTradeMetrics = ({
468
+ trades,
469
+ orderLog,
470
+ startTimestamp,
471
+ endTimestamp
472
+ }) => {
473
+ const normalizedTrades = trades.filter(
474
+ (trade) => isFiniteMetric(trade.timestamp) && isFiniteMetric(trade.pnl) && trade.timestamp > 0
475
+ ).sort((a, b) => a.timestamp - b.timestamp);
476
+ const pnls = normalizedTrades.map((trade) => trade.pnl);
477
+ const wins = pnls.filter((pnl) => pnl > 0).length;
478
+ const losses = pnls.filter((pnl) => pnl < 0).length;
479
+ const totalPnl = sum(pnls);
480
+ const grossProfit = pnls.filter((pnl) => pnl > 0).reduce((acc, pnl) => acc + pnl, 0);
481
+ const grossLoss = Math.abs(
482
+ pnls.filter((pnl) => pnl < 0).reduce((acc, pnl) => acc + pnl, 0)
483
+ );
484
+ const avgWin = wins ? grossProfit / wins : null;
485
+ const avgLoss = losses ? grossLoss / losses : null;
486
+ const firstTimestamp = startTimestamp ?? normalizedTrades[0]?.timestamp ?? null;
487
+ const lastTimestamp = endTimestamp ?? normalizedTrades[normalizedTrades.length - 1]?.timestamp ?? null;
488
+ const periodDays = isFiniteMetric(firstTimestamp) && isFiniteMetric(lastTimestamp) && lastTimestamp > firstTimestamp ? (lastTimestamp - firstTimestamp) / MS_IN_DAY : null;
489
+ const drawdown = calculateDrawdownStats(normalizedTrades, orderLog);
490
+ const monthly = /* @__PURE__ */ new Map();
491
+ const quarterly = /* @__PURE__ */ new Map();
492
+ for (const trade of normalizedTrades) {
493
+ const monthKey = getMonthKey(trade.timestamp);
494
+ const quarterKey = getQuarterKey(trade.timestamp);
495
+ const month = monthly.get(monthKey) ?? {
496
+ pnl: 0,
497
+ orders: 0,
498
+ wins: 0,
499
+ timestamp: trade.timestamp
500
+ };
501
+ month.pnl += trade.pnl;
502
+ month.orders += 1;
503
+ month.wins += trade.pnl > 0 ? 1 : 0;
504
+ month.timestamp = Math.min(month.timestamp, trade.timestamp);
505
+ monthly.set(monthKey, month);
506
+ quarterly.set(quarterKey, (quarterly.get(quarterKey) ?? 0) + trade.pnl);
507
+ }
508
+ const monthlyStats = [...monthly.entries()].sort(
509
+ ([a], [b]) => a.localeCompare(b)
510
+ );
511
+ const monthlyPnls = monthlyStats.map(([, stat]) => stat.pnl);
512
+ const monthlyWinRates = monthlyStats.map(
513
+ ([, stat]) => stat.orders ? stat.wins / stat.orders * 100 : 0
514
+ );
515
+ const positiveMonths = monthlyStats.filter(([, stat]) => stat.pnl > 0).length;
516
+ const p25Monthly = percentile(monthlyPnls, 0.25);
517
+ const p75Monthly = percentile(monthlyPnls, 0.75);
518
+ const dailyPnls = calculateDailyPnlSeries(
519
+ normalizedTrades,
520
+ startTimestamp,
521
+ endTimestamp
522
+ );
523
+ const dailyMean = dailyPnls.length ? mean(dailyPnls) : null;
524
+ const dailyStd = dailyMean === null ? null : calculateStd(dailyPnls, dailyMean);
525
+ const downsideDailyPnls = dailyPnls.map((pnl) => Math.min(pnl, 0));
526
+ const downsideStd = downsideDailyPnls.some((pnl) => pnl < 0) ? Math.sqrt(
527
+ downsideDailyPnls.reduce((acc, pnl) => acc + pnl ** 2, 0) / downsideDailyPnls.length
528
+ ) : null;
529
+ const annualizedPnl = dailyMean === null ? null : dailyMean * DAYS_IN_YEAR;
530
+ const approvedFlags = normalizedTrades.filter(
531
+ (trade) => typeof trade.approved === "boolean"
532
+ );
533
+ const slippageCosts = normalizedTrades.map((trade) => trade.slippageCost).filter(isFiniteMetric);
534
+ const pnlBeforeSlippage = normalizedTrades.reduce((acc, trade) => {
535
+ if (isFiniteMetric(trade.grossPnl)) {
536
+ return acc + trade.grossPnl;
537
+ }
538
+ if (isFiniteMetric(trade.slippageCost)) {
539
+ return acc + trade.pnl + trade.slippageCost;
540
+ }
541
+ return acc + trade.pnl;
542
+ }, 0);
543
+ const directionStats = normalizedTrades.reduce(
544
+ (acc, trade) => {
545
+ const direction = String(trade.direction ?? "").toUpperCase();
546
+ if (direction === "LONG") {
547
+ acc.longTrades += 1;
548
+ acc.longPnl += trade.pnl;
549
+ }
550
+ if (direction === "SHORT") {
551
+ acc.shortTrades += 1;
552
+ acc.shortPnl += trade.pnl;
553
+ }
554
+ return acc;
555
+ },
556
+ { longTrades: 0, shortTrades: 0, longPnl: 0, shortPnl: 0 }
557
+ );
558
+ return {
559
+ core: {
560
+ trades: normalizedTrades.length,
561
+ wins,
562
+ losses,
563
+ winRate: normalizedTrades.length ? wins / normalizedTrades.length * 100 : null,
564
+ totalPnl,
565
+ avgTrade: normalizedTrades.length ? totalPnl / normalizedTrades.length : null,
566
+ grossProfit,
567
+ grossLoss,
568
+ profitFactor: safeRatio(grossProfit, grossLoss),
569
+ payoffRatio: safeRatio(avgWin, avgLoss),
570
+ expectancy: normalizedTrades.length ? totalPnl / normalizedTrades.length : null,
571
+ tradesPerDay: periodDays && periodDays > 0 ? normalizedTrades.length / periodDays : null,
572
+ tradesPerWeek: periodDays && periodDays > 0 ? normalizedTrades.length / periodDays * 7 : null
573
+ },
574
+ risk: {
575
+ maxDrawdown: drawdown.absolute,
576
+ maxDrawdownPercent: drawdown.percent,
577
+ maxDrawdownToTotalProfit: totalPnl > 0 ? safeRatio(drawdown.absolute, totalPnl) : null,
578
+ maxDrawdownToGrossProfit: grossProfit > 0 ? safeRatio(drawdown.absolute, grossProfit) : null,
579
+ recoveryFactor: safeRatio(totalPnl, drawdown.absolute),
580
+ maxLossStreak: calculateLossStreak(normalizedTrades),
581
+ losingMonthsCount: monthlyStats.filter(([, stat]) => stat.pnl < 0).length,
582
+ worstMonthPnl: monthlyPnls.length ? Math.min(...monthlyPnls) : null,
583
+ worstRolling30dPnl: calculateWorstRollingPnl(normalizedTrades, 30),
584
+ worstRolling90dPnl: calculateWorstRollingPnl(normalizedTrades, 90)
585
+ },
586
+ stability: {
587
+ monthlyWinRate: monthlyWinRates.length ? mean(monthlyWinRates) : null,
588
+ positiveMonthsPercent: monthlyStats.length ? positiveMonths / monthlyStats.length * 100 : null,
589
+ quarterlyPnl: [...quarterly.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([quarter, pnl]) => ({ quarter, pnl })),
590
+ rolling365Pnl: normalizedTrades.length && isFiniteMetric(lastTimestamp) ? normalizedTrades.filter(
591
+ (trade) => lastTimestamp - trade.timestamp <= 365 * MS_IN_DAY
592
+ ).reduce((acc, trade) => acc + trade.pnl, 0) : null,
593
+ medianMonthlyPnl: medianValue(monthlyPnls),
594
+ iqrMonthlyPnl: p25Monthly === null || p75Monthly === null ? null : p75Monthly - p25Monthly,
595
+ top5ProfitShare: sumTopPositiveProfitShare(pnls, 5),
596
+ top10ProfitShare: sumTopPositiveProfitShare(pnls, 10)
597
+ },
598
+ distribution: {
599
+ medianTrade: medianValue(pnls),
600
+ p10Trade: percentile(pnls, 0.1),
601
+ p25Trade: percentile(pnls, 0.25),
602
+ p75Trade: percentile(pnls, 0.75),
603
+ p90Trade: percentile(pnls, 0.9),
604
+ largestWin: wins ? Math.max(...pnls.filter((pnl) => pnl > 0)) : null,
605
+ largestLoss: losses ? Math.min(...pnls.filter((pnl) => pnl < 0)) : null,
606
+ tailRatio: safeRatio(
607
+ percentile(pnls, 0.95),
608
+ Math.abs(percentile(pnls, 0.05) ?? 0)
609
+ ),
610
+ skewness: calculateSkewness(pnls)
611
+ },
612
+ riskAdjusted: {
613
+ sharpeDaily: dailyMean !== null && dailyStd !== null && dailyStd > 0 ? dailyMean / dailyStd * Math.sqrt(DAYS_IN_YEAR) : null,
614
+ sortinoDaily: dailyMean !== null && downsideStd !== null && downsideStd > 0 ? dailyMean / downsideStd * Math.sqrt(DAYS_IN_YEAR) : null,
615
+ calmar: safeRatio(annualizedPnl, drawdown.absolute),
616
+ mar: safeRatio(annualizedPnl, drawdown.absolute)
617
+ },
618
+ operational: {
619
+ avgSlippageCost: slippageCosts.length ? mean(slippageCosts) : null,
620
+ pnlBeforeSlippage: normalizedTrades.length ? pnlBeforeSlippage : null,
621
+ pnlAfterSlippage: totalPnl,
622
+ approvalRate: approvedFlags.length ? approvedFlags.filter((trade) => trade.approved).length / approvedFlags.length * 100 : null,
623
+ blockedProfitableTrades: normalizedTrades.filter(
624
+ (trade) => trade.blocked && trade.pnl > 0
625
+ ).length,
626
+ approvedLosingTrades: normalizedTrades.filter(
627
+ (trade) => trade.approved && trade.pnl < 0
628
+ ).length,
629
+ symbolConcentrationTop1: concentrationPercent(
630
+ normalizedTrades.filter((trade) => trade.symbol).map((trade) => ({ key: String(trade.symbol), pnl: trade.pnl })),
631
+ 1
632
+ ),
633
+ symbolConcentrationTop5: concentrationPercent(
634
+ normalizedTrades.filter((trade) => trade.symbol).map((trade) => ({ key: String(trade.symbol), pnl: trade.pnl })),
635
+ 5
636
+ ),
637
+ sessionConcentrationTop1: concentrationPercent(
638
+ normalizedTrades.map((trade) => ({
639
+ key: trade.session ?? resolveTradeSession(trade.timestamp),
640
+ pnl: trade.pnl
641
+ })),
642
+ 1
643
+ ),
644
+ longTrades: directionStats.longTrades,
645
+ shortTrades: directionStats.shortTrades,
646
+ longPnl: directionStats.longPnl,
647
+ shortPnl: directionStats.shortPnl,
648
+ exitBreakdown: calculateExitBreakdown(normalizedTrades)
649
+ }
650
+ };
651
+ };
354
652
  var computeMonthlyEquityStats = (positionLogData, opts) => {
355
653
  const MAR = opts?.mar ?? 0;
356
654
  const useSample = !!opts?.sampleStd;
@@ -378,12 +676,12 @@ var computeMonthlyEquityStats = (positionLogData, opts) => {
378
676
  const startTs = equityPoints2[0].ts;
379
677
  const endTs = equityPoints2[equityPoints2.length - 1].ts;
380
678
  const eomSeries = [];
381
- let monthCursor = (0, import_date_fns3.startOfMonth)(new Date(startTs));
382
- const lastMonth = (0, import_date_fns3.endOfMonth)(new Date(endTs));
679
+ let monthCursor = (0, import_date_fns.startOfMonth)(new Date(startTs));
680
+ const lastMonth = (0, import_date_fns.endOfMonth)(new Date(endTs));
383
681
  let i = 0;
384
682
  let lastAmount = equityPoints2[0].amount;
385
683
  while (monthCursor <= lastMonth) {
386
- const eom = (0, import_date_fns3.endOfMonth)(monthCursor);
684
+ const eom = (0, import_date_fns.endOfMonth)(monthCursor);
387
685
  const eomTs = eom.getTime();
388
686
  while (i < equityPoints2.length && equityPoints2[i].ts <= eomTs) {
389
687
  lastAmount = equityPoints2[i].amount;
@@ -391,7 +689,7 @@ var computeMonthlyEquityStats = (positionLogData, opts) => {
391
689
  }
392
690
  const key = `${eom.getFullYear()}-${String(eom.getMonth() + 1).padStart(2, "0")}`;
393
691
  eomSeries.push({ month: key, ts: eomTs, amount: lastAmount });
394
- monthCursor = (0, import_date_fns3.addMonths)(monthCursor, 1);
692
+ monthCursor = (0, import_date_fns.addMonths)(monthCursor, 1);
395
693
  }
396
694
  const monthlyReturns = [];
397
695
  for (let k = 1; k < eomSeries.length; k++) {
@@ -436,7 +734,7 @@ var calculateStatsFull = (positionLogData) => {
436
734
  const points = equityPoints(positionLogData);
437
735
  const startTs = points[0].ts;
438
736
  const endTs = points[points.length - 1].ts;
439
- const periodMs = (0, import_date_fns3.differenceInMilliseconds)(new Date(endTs), new Date(startTs));
737
+ const periodMs = (0, import_date_fns.differenceInMilliseconds)(new Date(endTs), new Date(startTs));
440
738
  const periodDays = periodMs / (1e3 * 60 * 60 * 24);
441
739
  const periodMonths = periodDays / 30.4375;
442
740
  const trades = positionLogData.length;
@@ -509,7 +807,10 @@ var calculateStatsFull = (positionLogData) => {
509
807
  };
510
808
  };
511
809
  var classifyMetric = (name, value) => {
512
- const { thresholds, direction } = TestThresholdsConfig[name];
810
+ const { thresholds, direction, neutralValue } = TestThresholdsConfig[name];
811
+ if (neutralValue !== void 0 && value === neutralValue) {
812
+ return "neutral";
813
+ }
513
814
  if (direction === "higher") {
514
815
  if (value >= thresholds[1]) return "success";
515
816
  if (value >= thresholds[0]) return "warning";
@@ -556,19 +857,60 @@ var getFormatted = (stat, key) => {
556
857
  level
557
858
  };
558
859
  };
860
+
861
+ // src/utils/timestamp.ts
862
+ var import_date_fns2 = require("date-fns");
863
+ var import_date_fns3 = require("date-fns");
864
+ var TIMELINE_STEP = 864e5;
865
+ var RUNTIME_STORAGE_DAY_OFFSET_MS = 6 * 60 * 60 * 1e3;
866
+ var getTimestamp = (days = 0) => {
867
+ if (days > 0) {
868
+ return (0, import_date_fns3.getUnixTime)((0, import_date_fns3.subDays)(/* @__PURE__ */ new Date(), days)) * 1e3;
869
+ }
870
+ return (0, import_date_fns3.getUnixTime)(/* @__PURE__ */ new Date()) * 1e3;
871
+ };
872
+ var getTimeline = (start = getTimestamp(BACKTEST_DEFAULT_DAYS), end = getTimestamp(), step = TIMELINE_STEP) => {
873
+ const res = new Array();
874
+ for (let ind = start; ind <= end; ind += step) {
875
+ res.push(ind);
876
+ }
877
+ return res;
878
+ };
879
+ var compactOrderLog = (timeline, orderLog) => {
880
+ const result = [];
881
+ let currentAmount = orderLog.length > 0 && orderLog[0].amount != null ? orderLog[0].amount : 100;
882
+ let orderLogCursor = 0;
883
+ for (let timelineIndex = 0; timelineIndex < timeline.length; timelineIndex++) {
884
+ const currentTimestamp = timeline[timelineIndex];
885
+ let lastApplicableOrderIndex = -1;
886
+ let nextCursor = orderLogCursor;
887
+ for (let checkIndex = orderLogCursor; checkIndex < orderLog.length; checkIndex++) {
888
+ const checkOrder = orderLog[checkIndex];
889
+ if (checkOrder.timestamp <= currentTimestamp) {
890
+ lastApplicableOrderIndex = checkIndex;
891
+ nextCursor = checkIndex + 1;
892
+ } else {
893
+ break;
894
+ }
895
+ }
896
+ if (lastApplicableOrderIndex !== -1) {
897
+ currentAmount = orderLog[lastApplicableOrderIndex].amount;
898
+ orderLogCursor = nextCursor;
899
+ }
900
+ result.push([currentTimestamp, currentAmount]);
901
+ }
902
+ return result;
903
+ };
559
904
  // Annotate the CommonJS export names for ESM import in node:
560
905
  0 && (module.exports = {
906
+ calculateAdvancedTradeMetrics,
561
907
  calculateMaxDrawdown,
562
908
  calculateStatsFull,
563
909
  classifyMetric,
564
910
  compactOrderLog,
565
- createTestSuite,
566
- generateName,
567
- generateParamGrid,
568
911
  getBacktestScore,
569
912
  getFormatted,
570
913
  getTimeline,
571
- mergeConfigs,
572
914
  parseTestName,
573
915
  sortBestTests
574
916
  });