tradelab 0.4.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 +38 -17
- package/bin/tradelab.js +68 -23
- package/dist/cjs/data.cjs +78 -53
- package/dist/cjs/index.cjs +1518 -211
- package/docs/README.md +6 -1
- package/docs/api-reference.md +7 -2
- package/docs/backtest-engine.md +40 -10
- package/docs/data-reporting-cli.md +7 -3
- package/docs/examples.md +281 -0
- package/package.json +1 -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/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
**tradelab** handles the simulation, sizing, exits, costs, and result exports
|
|
16
|
+
**tradelab** handles the simulation, sizing, exits, costs, and result exports; you bring the data and signal logic.
|
|
17
17
|
|
|
18
18
|
It works cleanly for a single-strategy backtest and scales up to portfolio runs, walk-forward testing, and detailed execution modeling. It is not a broker connector or a live trading tool.
|
|
19
19
|
|
|
@@ -31,6 +31,7 @@ npm install tradelab
|
|
|
31
31
|
- [Core concepts](#core-concepts)
|
|
32
32
|
- [Portfolio mode](#portfolio-mode)
|
|
33
33
|
- [Walk-forward optimization](#walk-forward-optimization)
|
|
34
|
+
- [Tick backtests](#tick-backtests)
|
|
34
35
|
- [Execution and cost modeling](#execution-and-cost-modeling)
|
|
35
36
|
- [Exports and reporting](#exports-and-reporting)
|
|
36
37
|
- [CLI](#cli)
|
|
@@ -43,9 +44,9 @@ npm install tradelab
|
|
|
43
44
|
|
|
44
45
|
| Area | What you get |
|
|
45
46
|
|---|---|
|
|
46
|
-
| **Engine** | Candle
|
|
47
|
-
| **Portfolio** | Multi-
|
|
48
|
-
| **Walk-forward** | Rolling train/test validation with parameter search |
|
|
47
|
+
| **Engine** | Candle and tick backtests with position sizing, exits, replay capture, and cost models |
|
|
48
|
+
| **Portfolio** | Multi-system shared-capital simulation with live capital locking and daily loss halts |
|
|
49
|
+
| **Walk-forward** | Rolling and anchored train/test validation with parameter search and stability summaries |
|
|
49
50
|
| **Data** | Yahoo Finance downloads, CSV import, and local cache helpers |
|
|
50
51
|
| **Costs** | Slippage, spread, and commission modeling |
|
|
51
52
|
| **Exports** | HTML reports, metrics JSON, and trade CSV |
|
|
@@ -177,19 +178,19 @@ The minimum viable signal is just `side`, `stop`, and `rr`. Start there and add
|
|
|
177
178
|
{
|
|
178
179
|
symbol, interval, range,
|
|
179
180
|
trades, // every realized leg, including partial exits
|
|
180
|
-
positions, // completed positions
|
|
181
|
+
positions, // completed positions - start here for analysis
|
|
181
182
|
metrics, // winRate, profitFactor, maxDrawdown, sharpe, ...
|
|
182
|
-
eqSeries, // [{ time, timestamp, equity }]
|
|
183
|
+
eqSeries, // [{ time, timestamp, equity }] - equity curve
|
|
183
184
|
replay, // visualization frames and events
|
|
184
185
|
}
|
|
185
186
|
```
|
|
186
187
|
|
|
187
188
|
**First checks after any run:**
|
|
188
189
|
|
|
189
|
-
- `metrics.trades`
|
|
190
|
-
- `metrics.profitFactor`
|
|
191
|
-
- `metrics.maxDrawdown`
|
|
192
|
-
- `metrics.sideBreakdown`
|
|
190
|
+
- `metrics.trades` - enough sample size to trust the numbers?
|
|
191
|
+
- `metrics.profitFactor` - do winners beat losers gross of costs?
|
|
192
|
+
- `metrics.maxDrawdown` - is the equity path survivable?
|
|
193
|
+
- `metrics.sideBreakdown` - does one side carry the whole result?
|
|
193
194
|
|
|
194
195
|
---
|
|
195
196
|
|
|
@@ -209,19 +210,20 @@ const result = backtestPortfolio({
|
|
|
209
210
|
});
|
|
210
211
|
```
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
Weights now act as default per-system allocation caps rather than pre-funded sleeves. Capital is locked only when a fill happens, `eqSeries` includes `lockedCapital` and `availableCapital`, later systems size against remaining live capital, and `maxDailyLossPct` on `backtestPortfolio()` can halt the whole book for the rest of the day.
|
|
213
214
|
|
|
214
215
|
---
|
|
215
216
|
|
|
216
217
|
## Walk-forward optimization
|
|
217
218
|
|
|
218
|
-
Use `walkForwardOptimize()` when one in-sample backtest is not enough. It
|
|
219
|
+
Use `walkForwardOptimize()` when one in-sample backtest is not enough. It supports rolling and anchored train/test windows across the full candle history.
|
|
219
220
|
|
|
220
221
|
```js
|
|
221
222
|
import { walkForwardOptimize } from "tradelab";
|
|
222
223
|
|
|
223
224
|
const wf = walkForwardOptimize({
|
|
224
225
|
candles,
|
|
226
|
+
mode: "anchored",
|
|
225
227
|
trainBars: 180,
|
|
226
228
|
testBars: 60,
|
|
227
229
|
stepBars: 60,
|
|
@@ -236,7 +238,25 @@ const wf = walkForwardOptimize({
|
|
|
236
238
|
});
|
|
237
239
|
```
|
|
238
240
|
|
|
239
|
-
Each window picks the best parameter set in training, then runs it blind on the test slice. The `windows` array
|
|
241
|
+
Each window picks the best parameter set in training, then runs it blind on the test slice. The `windows` array now includes out-of-sample trade count, profitability, and a per-window stability score. `bestParamsSummary` reports how stable the winners were across the full run.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Tick backtests
|
|
246
|
+
|
|
247
|
+
Use `backtestTicks()` when you want event-driven fills on tick or quote data without changing the result shape used by metrics, exports, or replay.
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
import { backtestTicks } from "tradelab";
|
|
251
|
+
|
|
252
|
+
const result = backtestTicks({
|
|
253
|
+
ticks,
|
|
254
|
+
queueFillProbability: 0.35,
|
|
255
|
+
signal,
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Market entries fill on the next tick, limit orders can fill at the touch with configurable queue probability, and stop exits use the existing cost model with stop-specific slippage if you provide it in `costs.slippageByKind.stop`.
|
|
240
260
|
|
|
241
261
|
---
|
|
242
262
|
|
|
@@ -313,7 +333,7 @@ npx tradelab portfolio \
|
|
|
313
333
|
# Walk-forward validation
|
|
314
334
|
npx tradelab walk-forward \
|
|
315
335
|
--source yahoo --symbol QQQ --interval 1d --period 2y \
|
|
316
|
-
--trainBars 180 --testBars 60
|
|
336
|
+
--trainBars 180 --testBars 60 --mode anchored
|
|
317
337
|
|
|
318
338
|
# Prefetch and cache data
|
|
319
339
|
npx tradelab prefetch --symbol SPY --interval 1d --period 1y
|
|
@@ -322,7 +342,7 @@ npx tradelab import-csv --csvPath ./data/spy.csv --symbol SPY --interval 1d
|
|
|
322
342
|
|
|
323
343
|
**Built-in strategies:** `ema-cross` · `buy-hold`
|
|
324
344
|
|
|
325
|
-
You can also point `--strategy` at a local module that exports `default(args)`, `createSignal(args)`, or `signal`.
|
|
345
|
+
You can also point `--strategy` at a local module that exports `default(args)`, `createSignal(args)`, or `signal` for `backtest`, or `signalFactory(params, args)` plus `parameterSets`/`createParameterSets(args)` for `walk-forward`.
|
|
326
346
|
|
|
327
347
|
---
|
|
328
348
|
|
|
@@ -361,6 +381,7 @@ const { fetchHistorical } = require("tradelab/data");
|
|
|
361
381
|
|---|---|
|
|
362
382
|
| [Backtest engine](docs/backtest-engine.md) | Signal contract, all options, result shape, portfolio mode, walk-forward |
|
|
363
383
|
| [Data, reporting, and CLI](docs/data-reporting-cli.md) | Data loading, cache behavior, exports, CLI reference |
|
|
384
|
+
| [Strategy examples](docs/examples.md) | Mean reversion, breakout, sentiment, LLM, and portfolio strategy patterns |
|
|
364
385
|
| [API reference](docs/api-reference.md) | Compact index of every public export |
|
|
365
386
|
|
|
366
387
|
---
|
|
@@ -368,7 +389,7 @@ const { fetchHistorical } = require("tradelab/data");
|
|
|
368
389
|
## Common mistakes
|
|
369
390
|
|
|
370
391
|
- Using unsorted candles or mixed intervals in a single series
|
|
371
|
-
- Reading `trades` as if they were always full positions
|
|
392
|
+
- Reading `trades` as if they were always full positions - use `positions` for top-line analysis
|
|
372
393
|
- Leaving costs at zero and overestimating edge
|
|
373
394
|
- Trusting one backtest without out-of-sample validation
|
|
374
395
|
- Debugging a strategy with `strict: false` when lookahead is possible
|
|
@@ -380,4 +401,4 @@ const { fetchHistorical } = require("tradelab/data");
|
|
|
380
401
|
- Node `18+` is required
|
|
381
402
|
- Yahoo downloads are cached under `output/data` by default
|
|
382
403
|
- CommonJS and ESM are both supported
|
|
383
|
-
- The engine is built for historical research
|
|
404
|
+
- The engine is built for historical research - not brokerage execution or full exchange microstructure simulation
|
package/bin/tradelab.js
CHANGED
|
@@ -48,6 +48,11 @@ function toList(value, fallback) {
|
|
|
48
48
|
.filter((item) => Number.isFinite(item));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function parseJsonValue(value, fallback = null) {
|
|
52
|
+
if (!value) return fallback;
|
|
53
|
+
return JSON.parse(String(value));
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
function createEmaCrossSignal({
|
|
52
57
|
fast = 10,
|
|
53
58
|
slow = 30,
|
|
@@ -133,13 +138,70 @@ async function loadStrategy(strategyArg, args) {
|
|
|
133
138
|
throw new Error(`Strategy module "${strategyArg}" must export default, createSignal, or signal`);
|
|
134
139
|
}
|
|
135
140
|
|
|
141
|
+
async function loadWalkForwardStrategy(strategyArg, args) {
|
|
142
|
+
if (!strategyArg || strategyArg === "ema-cross") {
|
|
143
|
+
const fasts = toList(args.fasts, [8, 10, 12]);
|
|
144
|
+
const slows = toList(args.slows, [20, 30, 40]);
|
|
145
|
+
const rrs = toList(args.rrs, [1.5, 2, 3]);
|
|
146
|
+
const parameterSets = [];
|
|
147
|
+
|
|
148
|
+
for (const fast of fasts) {
|
|
149
|
+
for (const slow of slows) {
|
|
150
|
+
if (fast >= slow) continue;
|
|
151
|
+
for (const rr of rrs) {
|
|
152
|
+
parameterSets.push({ fast, slow, rr });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
parameterSets,
|
|
159
|
+
signalFactory(params) {
|
|
160
|
+
return createEmaCrossSignal({
|
|
161
|
+
fast: params.fast,
|
|
162
|
+
slow: params.slow,
|
|
163
|
+
rr: params.rr,
|
|
164
|
+
stopLookback: toNumber(args.stopLookback, 15),
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const resolved = path.resolve(process.cwd(), strategyArg);
|
|
171
|
+
const module = await import(pathToFileURL(resolved).href);
|
|
172
|
+
if (typeof module.signalFactory !== "function") {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Walk-forward strategy module "${strategyArg}" must export signalFactory`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const parameterSets =
|
|
179
|
+
parseJsonValue(args.parameterSets) ??
|
|
180
|
+
(typeof module.createParameterSets === "function"
|
|
181
|
+
? await module.createParameterSets(args)
|
|
182
|
+
: module.parameterSets);
|
|
183
|
+
|
|
184
|
+
if (!Array.isArray(parameterSets) || parameterSets.length === 0) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Walk-forward strategy module "${strategyArg}" must provide parameterSets, createParameterSets(args), or --parameterSets`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
parameterSets,
|
|
192
|
+
signalFactory(params) {
|
|
193
|
+
return module.signalFactory(params, args);
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
136
198
|
function printHelp() {
|
|
137
199
|
console.log(`tradelab
|
|
138
200
|
|
|
139
201
|
Commands:
|
|
140
202
|
backtest Run a one-off backtest from Yahoo or CSV data
|
|
141
203
|
portfolio Run multiple CSV datasets as an equal-weight portfolio
|
|
142
|
-
walk-forward Run rolling train/test optimization
|
|
204
|
+
walk-forward Run rolling or anchored train/test optimization
|
|
143
205
|
prefetch Download Yahoo candles into the local cache
|
|
144
206
|
import-csv Normalize a CSV and save it into the local cache
|
|
145
207
|
|
|
@@ -257,26 +319,15 @@ async function commandWalkForward(args) {
|
|
|
257
319
|
csvPath: args.csvPath,
|
|
258
320
|
cache: args.cache !== "false",
|
|
259
321
|
});
|
|
260
|
-
const
|
|
261
|
-
const slows = toList(args.slows, [20, 30, 40]);
|
|
262
|
-
const rrs = toList(args.rrs, [1.5, 2, 3]);
|
|
263
|
-
const parameterSets = [];
|
|
264
|
-
|
|
265
|
-
for (const fast of fasts) {
|
|
266
|
-
for (const slow of slows) {
|
|
267
|
-
if (fast >= slow) continue;
|
|
268
|
-
for (const rr of rrs) {
|
|
269
|
-
parameterSets.push({ fast, slow, rr });
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
322
|
+
const walkForwardStrategy = await loadWalkForwardStrategy(args.strategy, args);
|
|
273
323
|
|
|
274
324
|
const result = walkForwardOptimize({
|
|
275
325
|
candles,
|
|
276
|
-
parameterSets,
|
|
326
|
+
parameterSets: walkForwardStrategy.parameterSets,
|
|
277
327
|
trainBars: toNumber(args.trainBars, 120),
|
|
278
328
|
testBars: toNumber(args.testBars, 40),
|
|
279
329
|
stepBars: toNumber(args.stepBars, toNumber(args.testBars, 40)),
|
|
330
|
+
mode: args.mode || "rolling",
|
|
280
331
|
scoreBy: args.scoreBy || "profitFactor",
|
|
281
332
|
backtestOptions: {
|
|
282
333
|
symbol: args.symbol || "DATA",
|
|
@@ -286,14 +337,7 @@ async function commandWalkForward(args) {
|
|
|
286
337
|
riskPct: toNumber(args.riskPct, 1),
|
|
287
338
|
warmupBars: toNumber(args.warmupBars, 20),
|
|
288
339
|
},
|
|
289
|
-
signalFactory
|
|
290
|
-
return createEmaCrossSignal({
|
|
291
|
-
fast: params.fast,
|
|
292
|
-
slow: params.slow,
|
|
293
|
-
rr: params.rr,
|
|
294
|
-
stopLookback: toNumber(args.stopLookback, 15),
|
|
295
|
-
});
|
|
296
|
-
},
|
|
340
|
+
signalFactory: walkForwardStrategy.signalFactory,
|
|
297
341
|
});
|
|
298
342
|
|
|
299
343
|
const metricsPath = exportMetricsJSON({
|
|
@@ -310,6 +354,7 @@ async function commandWalkForward(args) {
|
|
|
310
354
|
windows: result.windows.length,
|
|
311
355
|
positions: result.positions.length,
|
|
312
356
|
finalEquity: result.metrics.finalEquity,
|
|
357
|
+
bestParamsSummary: result.bestParamsSummary,
|
|
313
358
|
metricsPath,
|
|
314
359
|
},
|
|
315
360
|
null,
|
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,
|