tradelab 0.3.0 → 0.5.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.
- package/README.md +290 -130
- package/bin/tradelab.js +68 -23
- package/dist/cjs/data.cjs +78 -53
- package/dist/cjs/index.cjs +1518 -211
- package/docs/README.md +66 -0
- package/docs/api-reference.md +75 -0
- package/docs/backtest-engine.md +393 -0
- package/docs/data-reporting-cli.md +258 -0
- package/docs/examples.md +281 -0
- package/package.json +2 -1
- package/src/engine/backtestTicks.js +429 -0
- package/src/engine/barSystemRunner.js +963 -0
- package/src/engine/portfolio.js +191 -68
- package/src/engine/walkForward.js +106 -10
- package/src/index.js +1 -0
- package/src/metrics/buildMetrics.js +89 -63
- package/types/index.d.ts +77 -1
package/dist/cjs/data.cjs
CHANGED
|
@@ -215,56 +215,87 @@ function buildMetrics({
|
|
|
215
215
|
estBarMs,
|
|
216
216
|
eqSeries
|
|
217
217
|
}) {
|
|
218
|
-
const completedTrades = closed.filter((trade) => trade.exit.reason !== "SCALE");
|
|
219
|
-
const winningTrades = completedTrades.filter((trade) => trade.exit.pnl > 0);
|
|
220
|
-
const losingTrades = completedTrades.filter((trade) => trade.exit.pnl < 0);
|
|
221
|
-
const tradeRs = completedTrades.map(tradeRMultiple);
|
|
222
|
-
const totalR = sum(tradeRs);
|
|
223
|
-
const avgR = mean(tradeRs);
|
|
224
|
-
const labels = completedTrades.map(
|
|
225
|
-
(trade) => trade.exit.pnl > 0 ? "win" : trade.exit.pnl < 0 ? "loss" : "flat"
|
|
226
|
-
);
|
|
227
|
-
const { maxWin, maxLoss } = streaks(labels);
|
|
228
|
-
const tradePnls = completedTrades.map((trade) => trade.exit.pnl);
|
|
229
|
-
const expectancy = mean(tradePnls);
|
|
230
|
-
const tradeReturns = completedTrades.map(
|
|
231
|
-
(trade) => trade.exit.pnl / Math.max(1e-12, equityStart)
|
|
232
|
-
);
|
|
233
|
-
const tradeReturnStd = stddev(tradeReturns);
|
|
234
|
-
const sharpePerTrade = tradeReturnStd === 0 ? tradeReturns.length ? Infinity : 0 : mean(tradeReturns) / tradeReturnStd;
|
|
235
|
-
const sortinoPerTrade = sortino(tradeReturns);
|
|
236
|
-
const grossProfitPositions = sum(winningTrades.map((trade) => trade.exit.pnl));
|
|
237
|
-
const grossLossPositions = Math.abs(
|
|
238
|
-
sum(losingTrades.map((trade) => trade.exit.pnl))
|
|
239
|
-
);
|
|
240
|
-
const profitFactorPositions = grossLossPositions === 0 ? grossProfitPositions > 0 ? Infinity : 0 : grossProfitPositions / grossLossPositions;
|
|
241
218
|
const legs = [...closed].sort((left, right) => left.exit.time - right.exit.time);
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
const
|
|
219
|
+
const completedTrades = [];
|
|
220
|
+
const tradeRs = [];
|
|
221
|
+
const tradePnls = [];
|
|
222
|
+
const tradeReturns = [];
|
|
223
|
+
const holdDurationsMinutes = [];
|
|
224
|
+
const labels = [];
|
|
225
|
+
const longRs = [];
|
|
226
|
+
const shortRs = [];
|
|
227
|
+
let totalR = 0;
|
|
228
|
+
let realizedPnL = 0;
|
|
229
|
+
let winningTradeCount = 0;
|
|
230
|
+
let grossProfitPositions = 0;
|
|
231
|
+
let grossLossPositions = 0;
|
|
232
|
+
let grossProfitLegs = 0;
|
|
233
|
+
let grossLossLegs = 0;
|
|
234
|
+
let winningLegCount = 0;
|
|
235
|
+
let openBars = 0;
|
|
236
|
+
let longTradesCount = 0;
|
|
237
|
+
let longTradeWins = 0;
|
|
238
|
+
let longPnLSum = 0;
|
|
239
|
+
let shortTradesCount = 0;
|
|
240
|
+
let shortTradeWins = 0;
|
|
241
|
+
let shortPnLSum = 0;
|
|
247
242
|
let peakEquity = equityStart;
|
|
248
243
|
let currentEquity = equityStart;
|
|
249
244
|
let maxDrawdown = 0;
|
|
250
|
-
for (const
|
|
251
|
-
|
|
245
|
+
for (const trade of legs) {
|
|
246
|
+
const pnl = trade.exit.pnl;
|
|
247
|
+
realizedPnL += pnl;
|
|
248
|
+
if (pnl > 0) {
|
|
249
|
+
grossProfitLegs += pnl;
|
|
250
|
+
winningLegCount += 1;
|
|
251
|
+
} else if (pnl < 0) {
|
|
252
|
+
grossLossLegs += Math.abs(pnl);
|
|
253
|
+
}
|
|
254
|
+
currentEquity += pnl;
|
|
252
255
|
if (currentEquity > peakEquity) peakEquity = currentEquity;
|
|
253
256
|
const drawdown = (peakEquity - currentEquity) / Math.max(1e-12, peakEquity);
|
|
254
257
|
if (drawdown > maxDrawdown) maxDrawdown = drawdown;
|
|
258
|
+
if (trade.exit.reason === "SCALE") continue;
|
|
259
|
+
completedTrades.push(trade);
|
|
260
|
+
tradePnls.push(pnl);
|
|
261
|
+
tradeReturns.push(pnl / Math.max(1e-12, equityStart));
|
|
262
|
+
const tradeR = tradeRMultiple(trade);
|
|
263
|
+
tradeRs.push(tradeR);
|
|
264
|
+
totalR += tradeR;
|
|
265
|
+
labels.push(pnl > 0 ? "win" : pnl < 0 ? "loss" : "flat");
|
|
266
|
+
const holdMinutes = (trade.exit.time - trade.openTime) / (1e3 * 60);
|
|
267
|
+
holdDurationsMinutes.push(holdMinutes);
|
|
268
|
+
openBars += Math.max(1, Math.round((trade.exit.time - trade.openTime) / estBarMs));
|
|
269
|
+
if (pnl > 0) {
|
|
270
|
+
winningTradeCount += 1;
|
|
271
|
+
grossProfitPositions += pnl;
|
|
272
|
+
} else if (pnl < 0) {
|
|
273
|
+
grossLossPositions += Math.abs(pnl);
|
|
274
|
+
}
|
|
275
|
+
if (trade.side === "long") {
|
|
276
|
+
longTradesCount += 1;
|
|
277
|
+
longPnLSum += pnl;
|
|
278
|
+
longRs.push(tradeR);
|
|
279
|
+
if (pnl > 0) longTradeWins += 1;
|
|
280
|
+
} else if (trade.side === "short") {
|
|
281
|
+
shortTradesCount += 1;
|
|
282
|
+
shortPnLSum += pnl;
|
|
283
|
+
shortRs.push(tradeR);
|
|
284
|
+
if (pnl > 0) shortTradeWins += 1;
|
|
285
|
+
}
|
|
255
286
|
}
|
|
256
|
-
const
|
|
287
|
+
const avgR = mean(tradeRs);
|
|
288
|
+
const { maxWin, maxLoss } = streaks(labels);
|
|
289
|
+
const expectancy = mean(tradePnls);
|
|
290
|
+
const tradeReturnStd = stddev(tradeReturns);
|
|
291
|
+
const sharpePerTrade = tradeReturnStd === 0 ? tradeReturns.length ? Infinity : 0 : mean(tradeReturns) / tradeReturnStd;
|
|
292
|
+
const sortinoPerTrade = sortino(tradeReturns);
|
|
293
|
+
const profitFactorPositions = grossLossPositions === 0 ? grossProfitPositions > 0 ? Infinity : 0 : grossProfitPositions / grossLossPositions;
|
|
294
|
+
const profitFactorLegs = grossLossLegs === 0 ? grossProfitLegs > 0 ? Infinity : 0 : grossProfitLegs / grossLossLegs;
|
|
257
295
|
const returnPct = (equityFinal - equityStart) / Math.max(1e-12, equityStart);
|
|
258
296
|
const calmar = maxDrawdown === 0 ? returnPct > 0 ? Infinity : 0 : returnPct / maxDrawdown;
|
|
259
297
|
const totalBars = Math.max(1, candles.length);
|
|
260
|
-
const openBars = completedTrades.reduce((total, trade) => {
|
|
261
|
-
const barsHeld = Math.max(1, Math.round((trade.exit.time - trade.openTime) / estBarMs));
|
|
262
|
-
return total + barsHeld;
|
|
263
|
-
}, 0);
|
|
264
298
|
const exposurePct = openBars / totalBars;
|
|
265
|
-
const holdDurationsMinutes = completedTrades.map(
|
|
266
|
-
(trade) => (trade.exit.time - trade.openTime) / (1e3 * 60)
|
|
267
|
-
);
|
|
268
299
|
const avgHoldMin = mean(holdDurationsMinutes);
|
|
269
300
|
const equitySeries = eqSeries && eqSeries.length ? eqSeries : buildEquitySeriesFromLegs({ legs, equityStart });
|
|
270
301
|
const dailyReturnsSeries = dailyReturns(equitySeries);
|
|
@@ -272,12 +303,6 @@ function buildMetrics({
|
|
|
272
303
|
const sharpeDaily = dailyStd === 0 ? dailyReturnsSeries.length ? Infinity : 0 : mean(dailyReturnsSeries) / dailyStd;
|
|
273
304
|
const sortinoDaily = sortino(dailyReturnsSeries);
|
|
274
305
|
const dailyWinRate = dailyReturnsSeries.length ? dailyReturnsSeries.filter((value) => value > 0).length / dailyReturnsSeries.length : 0;
|
|
275
|
-
const longTrades = completedTrades.filter((trade) => trade.side === "long");
|
|
276
|
-
const shortTrades = completedTrades.filter((trade) => trade.side === "short");
|
|
277
|
-
const longRs = longTrades.map(tradeRMultiple);
|
|
278
|
-
const shortRs = shortTrades.map(tradeRMultiple);
|
|
279
|
-
const longPnls = longTrades.map((trade) => trade.exit.pnl);
|
|
280
|
-
const shortPnls = shortTrades.map((trade) => trade.exit.pnl);
|
|
281
306
|
const rDistribution = {
|
|
282
307
|
p10: percentile(tradeRs, 0.1),
|
|
283
308
|
p25: percentile(tradeRs, 0.25),
|
|
@@ -294,21 +319,21 @@ function buildMetrics({
|
|
|
294
319
|
};
|
|
295
320
|
const sideBreakdown = {
|
|
296
321
|
long: {
|
|
297
|
-
trades:
|
|
298
|
-
winRate:
|
|
299
|
-
avgPnL:
|
|
322
|
+
trades: longTradesCount,
|
|
323
|
+
winRate: longTradesCount ? longTradeWins / longTradesCount : 0,
|
|
324
|
+
avgPnL: longTradesCount ? longPnLSum / longTradesCount : 0,
|
|
300
325
|
avgR: mean(longRs)
|
|
301
326
|
},
|
|
302
327
|
short: {
|
|
303
|
-
trades:
|
|
304
|
-
winRate:
|
|
305
|
-
avgPnL:
|
|
328
|
+
trades: shortTradesCount,
|
|
329
|
+
winRate: shortTradesCount ? shortTradeWins / shortTradesCount : 0,
|
|
330
|
+
avgPnL: shortTradesCount ? shortPnLSum / shortTradesCount : 0,
|
|
306
331
|
avgR: mean(shortRs)
|
|
307
332
|
}
|
|
308
333
|
};
|
|
309
334
|
return {
|
|
310
335
|
trades: completedTrades.length,
|
|
311
|
-
winRate: completedTrades.length ?
|
|
336
|
+
winRate: completedTrades.length ? winningTradeCount / completedTrades.length : 0,
|
|
312
337
|
profitFactor: profitFactorPositions,
|
|
313
338
|
expectancy,
|
|
314
339
|
totalR,
|
|
@@ -330,8 +355,8 @@ function buildMetrics({
|
|
|
330
355
|
startEquity: equityStart,
|
|
331
356
|
profitFactor_pos: profitFactorPositions,
|
|
332
357
|
profitFactor_leg: profitFactorLegs,
|
|
333
|
-
winRate_pos: completedTrades.length ?
|
|
334
|
-
winRate_leg: legs.length ?
|
|
358
|
+
winRate_pos: completedTrades.length ? winningTradeCount / completedTrades.length : 0,
|
|
359
|
+
winRate_leg: legs.length ? winningLegCount / legs.length : 0,
|
|
335
360
|
sharpeDaily,
|
|
336
361
|
sortinoDaily,
|
|
337
362
|
sideBreakdown,
|