@tradejs/core 1.0.9 → 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-2ORZC66W.mjs → chunk-DXJ4NCFJ.mjs} +63 -6
  7. package/dist/chunk-OJPHC3S2.mjs +8 -0
  8. package/dist/{chunk-FNLPYYML.mjs → chunk-PNBS6J3G.mjs} +22 -1
  9. package/dist/constants.d.mts +25 -5
  10. package/dist/constants.d.ts +25 -5
  11. package/dist/constants.js +78 -9
  12. package/dist/constants.mjs +29 -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-UK6VTOUX.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.mjs CHANGED
@@ -1,6 +1,3 @@
1
- import {
2
- uuid
3
- } from "./chunk-AJK4NS7Y.mjs";
4
1
  import {
5
2
  absReturns,
6
3
  equityPoints,
@@ -11,82 +8,11 @@ import {
11
8
  } from "./chunk-AYC2QVKI.mjs";
12
9
  import {
13
10
  compactOrderLog,
14
- getTimeline,
15
- getTimestamp
16
- } from "./chunk-FNLPYYML.mjs";
11
+ getTimeline
12
+ } from "./chunk-PNBS6J3G.mjs";
17
13
  import {
18
- BACKTEST_DEFAULT_DAYS,
19
14
  TestThresholdsConfig
20
- } from "./chunk-2ORZC66W.mjs";
21
-
22
- // src/utils/grid.ts
23
- import _ from "lodash";
24
- var generateParamGrid = (paramOptions) => {
25
- const keys = Object.keys(paramOptions);
26
- const combinations = [];
27
- const helper = (index = 0, current = {}) => {
28
- if (index === keys.length) {
29
- combinations.push(current);
30
- return;
31
- }
32
- const key = keys[index];
33
- for (const value of paramOptions[key] || []) {
34
- const copiedValue = typeof value === "object" && value !== null ? structuredClone(value) : value;
35
- helper(index + 1, {
36
- ...current,
37
- [key]: copiedValue
38
- });
39
- }
40
- };
41
- helper();
42
- return combinations;
43
- };
44
- var generateName = (prefix) => `${prefix}_${uuid(6)}`;
45
- var mergeConfigs = (configs) => {
46
- const result = {};
47
- for (const config of configs) {
48
- for (const [key, value] of Object.entries(config)) {
49
- if (!result[key]) {
50
- result[key] = [];
51
- }
52
- const clonedValue = typeof value === "object" && value !== null ? _.cloneDeep(value) : value;
53
- const isDuplicate = result[key].some(
54
- (existing) => _.isEqual(existing, value)
55
- );
56
- if (!isDuplicate) {
57
- result[key].push(clonedValue);
58
- }
59
- }
60
- }
61
- for (const key in result) {
62
- if (result[key].every((v) => typeof v === "number")) {
63
- result[key] = _.sortBy(result[key]);
64
- }
65
- }
66
- return result;
67
- };
68
- var createTestSuite = (userName, tickers, strategyName, backtestConfig, connectorName) => {
69
- const start = getTimestamp(BACKTEST_DEFAULT_DAYS);
70
- const end = getTimestamp();
71
- const testSuiteId = uuid(6);
72
- const paramGrid = generateParamGrid(backtestConfig);
73
- return tickers.flatMap(
74
- (symbol) => paramGrid.map((params) => {
75
- const testId = uuid(6);
76
- return {
77
- userName,
78
- name: `${symbol}_${testSuiteId}_${testId}`,
79
- testId,
80
- testSuiteId,
81
- symbol,
82
- options: { start, end },
83
- strategyName,
84
- strategyConfig: params,
85
- connectorName
86
- };
87
- })
88
- );
89
- };
15
+ } from "./chunk-DXJ4NCFJ.mjs";
90
16
 
91
17
  // src/utils/tests.ts
92
18
  var parseTestName = (testName) => {
@@ -133,6 +59,412 @@ var calculateMaxDrawdown = (amounts) => {
133
59
  }
134
60
  return maxDrawdown;
135
61
  };
62
+ var MS_IN_DAY = 24 * 60 * 60 * 1e3;
63
+ var DAYS_IN_YEAR = 365;
64
+ var isFiniteMetric = (value) => typeof value === "number" && Number.isFinite(value);
65
+ var safeRatio = (numerator, denominator) => {
66
+ if (!isFiniteMetric(numerator) || !isFiniteMetric(denominator)) {
67
+ return null;
68
+ }
69
+ if (Math.abs(denominator) <= Number.EPSILON) {
70
+ return null;
71
+ }
72
+ return numerator / denominator;
73
+ };
74
+ var percentile = (values, p) => {
75
+ if (!values.length) {
76
+ return null;
77
+ }
78
+ const sorted = [...values].sort((a, b) => a - b);
79
+ const index = (sorted.length - 1) * p;
80
+ const lower = Math.floor(index);
81
+ const upper = Math.ceil(index);
82
+ if (lower === upper) {
83
+ return sorted[lower];
84
+ }
85
+ const weight = index - lower;
86
+ return sorted[lower] * (1 - weight) + sorted[upper] * weight;
87
+ };
88
+ var medianValue = (values) => percentile(values, 0.5);
89
+ var getMonthKey = (timestamp) => {
90
+ const date = new Date(timestamp);
91
+ return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}`;
92
+ };
93
+ var getQuarterKey = (timestamp) => {
94
+ const date = new Date(timestamp);
95
+ return `${date.getUTCFullYear()} Q${Math.floor(date.getUTCMonth() / 3) + 1}`;
96
+ };
97
+ var resolveTradeSession = (timestamp) => {
98
+ const hour = new Date(timestamp).getUTCHours();
99
+ if (hour < 8) {
100
+ return "Asia";
101
+ }
102
+ if (hour < 16) {
103
+ return "Europe";
104
+ }
105
+ return "US";
106
+ };
107
+ var getDateKey = (timestamp) => {
108
+ const date = new Date(timestamp);
109
+ return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
110
+ };
111
+ var calculateDrawdownStats = (trades, orderLog) => {
112
+ const explicitPoints = (orderLog ?? []).map((point) => ({ timestamp: point[0], amount: point[1] })).filter(
113
+ (point) => isFiniteMetric(point.timestamp) && isFiniteMetric(point.amount)
114
+ ).sort((a, b) => a.timestamp - b.timestamp);
115
+ const points = explicitPoints.length ? explicitPoints : trades.slice().sort((a, b) => a.timestamp - b.timestamp).reduce(
116
+ (acc, trade) => {
117
+ const previous = acc[acc.length - 1]?.amount ?? 0;
118
+ acc.push({
119
+ timestamp: trade.timestamp,
120
+ amount: previous + trade.pnl
121
+ });
122
+ return acc;
123
+ },
124
+ [{ timestamp: trades[0]?.timestamp ?? 0, amount: 0 }]
125
+ );
126
+ if (!points.length) {
127
+ return { absolute: null, percent: null };
128
+ }
129
+ let peak = points[0].amount;
130
+ let maxAbsolute = 0;
131
+ let maxPercent = 0;
132
+ for (const point of points) {
133
+ if (point.amount > peak) {
134
+ peak = point.amount;
135
+ }
136
+ const absolute = peak - point.amount;
137
+ maxAbsolute = Math.max(maxAbsolute, absolute);
138
+ if (peak > 0) {
139
+ maxPercent = Math.max(maxPercent, absolute / peak * 100);
140
+ }
141
+ }
142
+ return { absolute: maxAbsolute, percent: maxPercent };
143
+ };
144
+ var calculateWorstRollingPnl = (trades, days) => {
145
+ if (!trades.length) {
146
+ return null;
147
+ }
148
+ const sorted = trades.slice().sort((a, b) => a.timestamp - b.timestamp);
149
+ const windowMs = days * MS_IN_DAY;
150
+ let start = 0;
151
+ let rollingPnl = 0;
152
+ let worstPnl = 0;
153
+ for (let end = 0; end < sorted.length; end += 1) {
154
+ rollingPnl += sorted[end].pnl;
155
+ while (start <= end && sorted[end].timestamp - sorted[start].timestamp > windowMs) {
156
+ rollingPnl -= sorted[start].pnl;
157
+ start += 1;
158
+ }
159
+ worstPnl = Math.min(worstPnl, rollingPnl);
160
+ }
161
+ return worstPnl;
162
+ };
163
+ var calculateLossStreak = (trades) => {
164
+ let current = 0;
165
+ let max = 0;
166
+ for (const trade of trades.slice().sort((a, b) => a.timestamp - b.timestamp)) {
167
+ if (trade.pnl < 0) {
168
+ current += 1;
169
+ max = Math.max(max, current);
170
+ continue;
171
+ }
172
+ current = 0;
173
+ }
174
+ return max;
175
+ };
176
+ var normalizeExitReason = (reason) => {
177
+ const normalized = String(reason ?? "").trim().toLowerCase();
178
+ if (normalized === "tp" || normalized === "take_profit") {
179
+ return "takeProfit";
180
+ }
181
+ if (normalized === "sl" || normalized === "stop_loss") {
182
+ return "stopLoss";
183
+ }
184
+ if (normalized === "exit" || normalized === "close" || normalized === "closed") {
185
+ return "exit";
186
+ }
187
+ return "unknown";
188
+ };
189
+ var calculateExitBreakdown = (trades) => {
190
+ const counts = {
191
+ takeProfit: 0,
192
+ stopLoss: 0,
193
+ exit: 0,
194
+ unknown: 0
195
+ };
196
+ for (const trade of trades) {
197
+ counts[normalizeExitReason(trade.exitReason)] += 1;
198
+ }
199
+ const total = trades.length;
200
+ const bucket = (count) => ({
201
+ count,
202
+ share: total ? count / total * 100 : null
203
+ });
204
+ return {
205
+ takeProfit: bucket(counts.takeProfit),
206
+ stopLoss: bucket(counts.stopLoss),
207
+ exit: bucket(counts.exit),
208
+ unknown: bucket(counts.unknown)
209
+ };
210
+ };
211
+ var calculateDailyPnlSeries = (trades, startTimestamp, endTimestamp) => {
212
+ const approvedTrades = trades.filter((trade) => trade.approved !== false);
213
+ if (!approvedTrades.length) {
214
+ return [];
215
+ }
216
+ const firstTimestamp = startTimestamp ?? Math.min(...approvedTrades.map((trade) => trade.timestamp));
217
+ const lastTimestamp = endTimestamp ?? Math.max(...approvedTrades.map((trade) => trade.timestamp));
218
+ if (!isFiniteMetric(firstTimestamp) || !isFiniteMetric(lastTimestamp) || lastTimestamp < firstTimestamp) {
219
+ return [];
220
+ }
221
+ const startDate = Date.UTC(
222
+ new Date(firstTimestamp).getUTCFullYear(),
223
+ new Date(firstTimestamp).getUTCMonth(),
224
+ new Date(firstTimestamp).getUTCDate()
225
+ );
226
+ const endDate = Date.UTC(
227
+ new Date(lastTimestamp).getUTCFullYear(),
228
+ new Date(lastTimestamp).getUTCMonth(),
229
+ new Date(lastTimestamp).getUTCDate()
230
+ );
231
+ const daily = /* @__PURE__ */ new Map();
232
+ for (let ts = startDate; ts <= endDate; ts += MS_IN_DAY) {
233
+ daily.set(getDateKey(ts), 0);
234
+ }
235
+ for (const trade of approvedTrades) {
236
+ const key = getDateKey(trade.timestamp);
237
+ daily.set(key, (daily.get(key) ?? 0) + trade.pnl);
238
+ }
239
+ return [...daily.values()];
240
+ };
241
+ var calculateStd = (values, valueMean) => {
242
+ if (!values.length) {
243
+ return 0;
244
+ }
245
+ return Math.sqrt(
246
+ values.reduce((acc, value) => acc + (value - valueMean) ** 2, 0) / values.length
247
+ );
248
+ };
249
+ var calculateSkewness = (values) => {
250
+ if (values.length < 3) {
251
+ return null;
252
+ }
253
+ const valueMean = mean(values);
254
+ const std = calculateStd(values, valueMean);
255
+ if (std <= Number.EPSILON) {
256
+ return null;
257
+ }
258
+ return values.reduce((acc, value) => acc + ((value - valueMean) / std) ** 3, 0) / values.length;
259
+ };
260
+ var sumTopPositiveProfitShare = (pnls, count) => {
261
+ const grossProfit = pnls.filter((pnl) => pnl > 0).reduce((acc, pnl) => acc + pnl, 0);
262
+ if (grossProfit <= 0) {
263
+ return null;
264
+ }
265
+ const topProfit = pnls.filter((pnl) => pnl > 0).sort((a, b) => b - a).slice(0, count).reduce((acc, pnl) => acc + pnl, 0);
266
+ return topProfit / grossProfit * 100;
267
+ };
268
+ var concentrationPercent = (items, limit) => {
269
+ const totals = /* @__PURE__ */ new Map();
270
+ for (const item of items) {
271
+ totals.set(item.key, (totals.get(item.key) ?? 0) + Math.abs(item.pnl));
272
+ }
273
+ const totalAbsPnl = [...totals.values()].reduce(
274
+ (acc, value) => acc + value,
275
+ 0
276
+ );
277
+ if (totalAbsPnl <= 0) {
278
+ return null;
279
+ }
280
+ const topAbsPnl = [...totals.values()].sort((a, b) => b - a).slice(0, limit).reduce((acc, value) => acc + value, 0);
281
+ return topAbsPnl / totalAbsPnl * 100;
282
+ };
283
+ var calculateAdvancedTradeMetrics = ({
284
+ trades,
285
+ orderLog,
286
+ startTimestamp,
287
+ endTimestamp
288
+ }) => {
289
+ const normalizedTrades = trades.filter(
290
+ (trade) => isFiniteMetric(trade.timestamp) && isFiniteMetric(trade.pnl) && trade.timestamp > 0
291
+ ).sort((a, b) => a.timestamp - b.timestamp);
292
+ const pnls = normalizedTrades.map((trade) => trade.pnl);
293
+ const wins = pnls.filter((pnl) => pnl > 0).length;
294
+ const losses = pnls.filter((pnl) => pnl < 0).length;
295
+ const totalPnl = sum(pnls);
296
+ const grossProfit = pnls.filter((pnl) => pnl > 0).reduce((acc, pnl) => acc + pnl, 0);
297
+ const grossLoss = Math.abs(
298
+ pnls.filter((pnl) => pnl < 0).reduce((acc, pnl) => acc + pnl, 0)
299
+ );
300
+ const avgWin = wins ? grossProfit / wins : null;
301
+ const avgLoss = losses ? grossLoss / losses : null;
302
+ const firstTimestamp = startTimestamp ?? normalizedTrades[0]?.timestamp ?? null;
303
+ const lastTimestamp = endTimestamp ?? normalizedTrades[normalizedTrades.length - 1]?.timestamp ?? null;
304
+ const periodDays = isFiniteMetric(firstTimestamp) && isFiniteMetric(lastTimestamp) && lastTimestamp > firstTimestamp ? (lastTimestamp - firstTimestamp) / MS_IN_DAY : null;
305
+ const drawdown = calculateDrawdownStats(normalizedTrades, orderLog);
306
+ const monthly = /* @__PURE__ */ new Map();
307
+ const quarterly = /* @__PURE__ */ new Map();
308
+ for (const trade of normalizedTrades) {
309
+ const monthKey = getMonthKey(trade.timestamp);
310
+ const quarterKey = getQuarterKey(trade.timestamp);
311
+ const month = monthly.get(monthKey) ?? {
312
+ pnl: 0,
313
+ orders: 0,
314
+ wins: 0,
315
+ timestamp: trade.timestamp
316
+ };
317
+ month.pnl += trade.pnl;
318
+ month.orders += 1;
319
+ month.wins += trade.pnl > 0 ? 1 : 0;
320
+ month.timestamp = Math.min(month.timestamp, trade.timestamp);
321
+ monthly.set(monthKey, month);
322
+ quarterly.set(quarterKey, (quarterly.get(quarterKey) ?? 0) + trade.pnl);
323
+ }
324
+ const monthlyStats = [...monthly.entries()].sort(
325
+ ([a], [b]) => a.localeCompare(b)
326
+ );
327
+ const monthlyPnls = monthlyStats.map(([, stat]) => stat.pnl);
328
+ const monthlyWinRates = monthlyStats.map(
329
+ ([, stat]) => stat.orders ? stat.wins / stat.orders * 100 : 0
330
+ );
331
+ const positiveMonths = monthlyStats.filter(([, stat]) => stat.pnl > 0).length;
332
+ const p25Monthly = percentile(monthlyPnls, 0.25);
333
+ const p75Monthly = percentile(monthlyPnls, 0.75);
334
+ const dailyPnls = calculateDailyPnlSeries(
335
+ normalizedTrades,
336
+ startTimestamp,
337
+ endTimestamp
338
+ );
339
+ const dailyMean = dailyPnls.length ? mean(dailyPnls) : null;
340
+ const dailyStd = dailyMean === null ? null : calculateStd(dailyPnls, dailyMean);
341
+ const downsideDailyPnls = dailyPnls.map((pnl) => Math.min(pnl, 0));
342
+ const downsideStd = downsideDailyPnls.some((pnl) => pnl < 0) ? Math.sqrt(
343
+ downsideDailyPnls.reduce((acc, pnl) => acc + pnl ** 2, 0) / downsideDailyPnls.length
344
+ ) : null;
345
+ const annualizedPnl = dailyMean === null ? null : dailyMean * DAYS_IN_YEAR;
346
+ const approvedFlags = normalizedTrades.filter(
347
+ (trade) => typeof trade.approved === "boolean"
348
+ );
349
+ const slippageCosts = normalizedTrades.map((trade) => trade.slippageCost).filter(isFiniteMetric);
350
+ const pnlBeforeSlippage = normalizedTrades.reduce((acc, trade) => {
351
+ if (isFiniteMetric(trade.grossPnl)) {
352
+ return acc + trade.grossPnl;
353
+ }
354
+ if (isFiniteMetric(trade.slippageCost)) {
355
+ return acc + trade.pnl + trade.slippageCost;
356
+ }
357
+ return acc + trade.pnl;
358
+ }, 0);
359
+ const directionStats = normalizedTrades.reduce(
360
+ (acc, trade) => {
361
+ const direction = String(trade.direction ?? "").toUpperCase();
362
+ if (direction === "LONG") {
363
+ acc.longTrades += 1;
364
+ acc.longPnl += trade.pnl;
365
+ }
366
+ if (direction === "SHORT") {
367
+ acc.shortTrades += 1;
368
+ acc.shortPnl += trade.pnl;
369
+ }
370
+ return acc;
371
+ },
372
+ { longTrades: 0, shortTrades: 0, longPnl: 0, shortPnl: 0 }
373
+ );
374
+ return {
375
+ core: {
376
+ trades: normalizedTrades.length,
377
+ wins,
378
+ losses,
379
+ winRate: normalizedTrades.length ? wins / normalizedTrades.length * 100 : null,
380
+ totalPnl,
381
+ avgTrade: normalizedTrades.length ? totalPnl / normalizedTrades.length : null,
382
+ grossProfit,
383
+ grossLoss,
384
+ profitFactor: safeRatio(grossProfit, grossLoss),
385
+ payoffRatio: safeRatio(avgWin, avgLoss),
386
+ expectancy: normalizedTrades.length ? totalPnl / normalizedTrades.length : null,
387
+ tradesPerDay: periodDays && periodDays > 0 ? normalizedTrades.length / periodDays : null,
388
+ tradesPerWeek: periodDays && periodDays > 0 ? normalizedTrades.length / periodDays * 7 : null
389
+ },
390
+ risk: {
391
+ maxDrawdown: drawdown.absolute,
392
+ maxDrawdownPercent: drawdown.percent,
393
+ maxDrawdownToTotalProfit: totalPnl > 0 ? safeRatio(drawdown.absolute, totalPnl) : null,
394
+ maxDrawdownToGrossProfit: grossProfit > 0 ? safeRatio(drawdown.absolute, grossProfit) : null,
395
+ recoveryFactor: safeRatio(totalPnl, drawdown.absolute),
396
+ maxLossStreak: calculateLossStreak(normalizedTrades),
397
+ losingMonthsCount: monthlyStats.filter(([, stat]) => stat.pnl < 0).length,
398
+ worstMonthPnl: monthlyPnls.length ? Math.min(...monthlyPnls) : null,
399
+ worstRolling30dPnl: calculateWorstRollingPnl(normalizedTrades, 30),
400
+ worstRolling90dPnl: calculateWorstRollingPnl(normalizedTrades, 90)
401
+ },
402
+ stability: {
403
+ monthlyWinRate: monthlyWinRates.length ? mean(monthlyWinRates) : null,
404
+ positiveMonthsPercent: monthlyStats.length ? positiveMonths / monthlyStats.length * 100 : null,
405
+ quarterlyPnl: [...quarterly.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([quarter, pnl]) => ({ quarter, pnl })),
406
+ rolling365Pnl: normalizedTrades.length && isFiniteMetric(lastTimestamp) ? normalizedTrades.filter(
407
+ (trade) => lastTimestamp - trade.timestamp <= 365 * MS_IN_DAY
408
+ ).reduce((acc, trade) => acc + trade.pnl, 0) : null,
409
+ medianMonthlyPnl: medianValue(monthlyPnls),
410
+ iqrMonthlyPnl: p25Monthly === null || p75Monthly === null ? null : p75Monthly - p25Monthly,
411
+ top5ProfitShare: sumTopPositiveProfitShare(pnls, 5),
412
+ top10ProfitShare: sumTopPositiveProfitShare(pnls, 10)
413
+ },
414
+ distribution: {
415
+ medianTrade: medianValue(pnls),
416
+ p10Trade: percentile(pnls, 0.1),
417
+ p25Trade: percentile(pnls, 0.25),
418
+ p75Trade: percentile(pnls, 0.75),
419
+ p90Trade: percentile(pnls, 0.9),
420
+ largestWin: wins ? Math.max(...pnls.filter((pnl) => pnl > 0)) : null,
421
+ largestLoss: losses ? Math.min(...pnls.filter((pnl) => pnl < 0)) : null,
422
+ tailRatio: safeRatio(
423
+ percentile(pnls, 0.95),
424
+ Math.abs(percentile(pnls, 0.05) ?? 0)
425
+ ),
426
+ skewness: calculateSkewness(pnls)
427
+ },
428
+ riskAdjusted: {
429
+ sharpeDaily: dailyMean !== null && dailyStd !== null && dailyStd > 0 ? dailyMean / dailyStd * Math.sqrt(DAYS_IN_YEAR) : null,
430
+ sortinoDaily: dailyMean !== null && downsideStd !== null && downsideStd > 0 ? dailyMean / downsideStd * Math.sqrt(DAYS_IN_YEAR) : null,
431
+ calmar: safeRatio(annualizedPnl, drawdown.absolute),
432
+ mar: safeRatio(annualizedPnl, drawdown.absolute)
433
+ },
434
+ operational: {
435
+ avgSlippageCost: slippageCosts.length ? mean(slippageCosts) : null,
436
+ pnlBeforeSlippage: normalizedTrades.length ? pnlBeforeSlippage : null,
437
+ pnlAfterSlippage: totalPnl,
438
+ approvalRate: approvedFlags.length ? approvedFlags.filter((trade) => trade.approved).length / approvedFlags.length * 100 : null,
439
+ blockedProfitableTrades: normalizedTrades.filter(
440
+ (trade) => trade.blocked && trade.pnl > 0
441
+ ).length,
442
+ approvedLosingTrades: normalizedTrades.filter(
443
+ (trade) => trade.approved && trade.pnl < 0
444
+ ).length,
445
+ symbolConcentrationTop1: concentrationPercent(
446
+ normalizedTrades.filter((trade) => trade.symbol).map((trade) => ({ key: String(trade.symbol), pnl: trade.pnl })),
447
+ 1
448
+ ),
449
+ symbolConcentrationTop5: concentrationPercent(
450
+ normalizedTrades.filter((trade) => trade.symbol).map((trade) => ({ key: String(trade.symbol), pnl: trade.pnl })),
451
+ 5
452
+ ),
453
+ sessionConcentrationTop1: concentrationPercent(
454
+ normalizedTrades.map((trade) => ({
455
+ key: trade.session ?? resolveTradeSession(trade.timestamp),
456
+ pnl: trade.pnl
457
+ })),
458
+ 1
459
+ ),
460
+ longTrades: directionStats.longTrades,
461
+ shortTrades: directionStats.shortTrades,
462
+ longPnl: directionStats.longPnl,
463
+ shortPnl: directionStats.shortPnl,
464
+ exitBreakdown: calculateExitBreakdown(normalizedTrades)
465
+ }
466
+ };
467
+ };
136
468
  var computeMonthlyEquityStats = (positionLogData, opts) => {
137
469
  const MAR = opts?.mar ?? 0;
138
470
  const useSample = !!opts?.sampleStd;
@@ -291,7 +623,10 @@ var calculateStatsFull = (positionLogData) => {
291
623
  };
292
624
  };
293
625
  var classifyMetric = (name, value) => {
294
- const { thresholds, direction } = TestThresholdsConfig[name];
626
+ const { thresholds, direction, neutralValue } = TestThresholdsConfig[name];
627
+ if (neutralValue !== void 0 && value === neutralValue) {
628
+ return "neutral";
629
+ }
295
630
  if (direction === "higher") {
296
631
  if (value >= thresholds[1]) return "success";
297
632
  if (value >= thresholds[0]) return "warning";
@@ -339,17 +674,14 @@ var getFormatted = (stat, key) => {
339
674
  };
340
675
  };
341
676
  export {
677
+ calculateAdvancedTradeMetrics,
342
678
  calculateMaxDrawdown,
343
679
  calculateStatsFull,
344
680
  classifyMetric,
345
681
  compactOrderLog,
346
- createTestSuite,
347
- generateName,
348
- generateParamGrid,
349
682
  getBacktestScore,
350
683
  getFormatted,
351
684
  getTimeline,
352
- mergeConfigs,
353
685
  parseTestName,
354
686
  sortBestTests
355
687
  };