tradelab 0.5.0 → 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 (54) hide show
  1. package/README.md +89 -41
  2. package/bin/tradelab.js +276 -30
  3. package/dist/cjs/data.cjs +134 -104
  4. package/dist/cjs/index.cjs +378 -177
  5. package/dist/cjs/live.cjs +3350 -0
  6. package/docs/README.md +21 -9
  7. package/docs/api-reference.md +87 -29
  8. package/docs/backtest-engine.md +37 -53
  9. package/docs/data-reporting-cli.md +60 -34
  10. package/docs/examples.md +6 -12
  11. package/docs/live-trading.md +186 -0
  12. package/examples/yahooEmaCross.js +1 -6
  13. package/package.json +18 -3
  14. package/src/data/csv.js +24 -14
  15. package/src/data/index.js +1 -5
  16. package/src/data/yahoo.js +6 -19
  17. package/src/engine/backtest.js +137 -144
  18. package/src/engine/backtestTicks.js +89 -37
  19. package/src/engine/barSystemRunner.js +182 -118
  20. package/src/engine/execution.js +11 -39
  21. package/src/engine/portfolio.js +54 -6
  22. package/src/engine/walkForward.js +37 -14
  23. package/src/index.js +2 -11
  24. package/src/live/broker/alpaca.js +254 -0
  25. package/src/live/broker/binance.js +351 -0
  26. package/src/live/broker/coinbase.js +339 -0
  27. package/src/live/broker/interactiveBrokers.js +123 -0
  28. package/src/live/broker/interface.js +74 -0
  29. package/src/live/clock.js +56 -0
  30. package/src/live/engine/candleAggregator.js +154 -0
  31. package/src/live/engine/liveEngine.js +694 -0
  32. package/src/live/engine/paperEngine.js +453 -0
  33. package/src/live/engine/riskManager.js +185 -0
  34. package/src/live/engine/stateManager.js +112 -0
  35. package/src/live/events.js +48 -0
  36. package/src/live/feed/brokerFeed.js +35 -0
  37. package/src/live/feed/interface.js +28 -0
  38. package/src/live/feed/pollingFeed.js +105 -0
  39. package/src/live/index.js +27 -0
  40. package/src/live/logger.js +82 -0
  41. package/src/live/orchestrator.js +133 -0
  42. package/src/live/storage/interface.js +36 -0
  43. package/src/live/storage/jsonFileStorage.js +112 -0
  44. package/src/metrics/buildMetrics.js +18 -41
  45. package/src/reporting/exportBacktestArtifacts.js +1 -4
  46. package/src/reporting/exportTradesCsv.js +2 -7
  47. package/src/reporting/renderHtmlReport.js +8 -13
  48. package/src/utils/indicators.js +1 -2
  49. package/src/utils/positionSizing.js +16 -2
  50. package/src/utils/time.js +4 -12
  51. package/templates/report.html +23 -9
  52. package/templates/report.js +83 -69
  53. package/types/index.d.ts +21 -3
  54. package/types/live.d.ts +382 -0
@@ -0,0 +1,112 @@
1
+ import fs from "node:fs";
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { StorageProvider } from "./interface.js";
6
+
7
+ function sanitizeNamespace(namespace) {
8
+ return String(namespace || "default").replace(/[^a-zA-Z0-9._-]/g, "_");
9
+ }
10
+
11
+ async function ensureDir(dirPath) {
12
+ await fsp.mkdir(dirPath, { recursive: true });
13
+ }
14
+
15
+ async function readJsonFile(filePath) {
16
+ try {
17
+ const raw = await fsp.readFile(filePath, "utf8");
18
+ return JSON.parse(raw);
19
+ } catch (error) {
20
+ if (error && error.code === "ENOENT") return null;
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ async function writeJsonAtomic(filePath, payload) {
26
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${Math.random()
27
+ .toString(16)
28
+ .slice(2)}.tmp`;
29
+ await fsp.writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf8");
30
+ await fsp.rename(tmpPath, filePath);
31
+ }
32
+
33
+ async function appendJsonLine(filePath, payload) {
34
+ await ensureDir(path.dirname(filePath));
35
+ await fsp.appendFile(filePath, `${JSON.stringify(payload)}\n`, "utf8");
36
+ }
37
+
38
+ async function readJsonLines(filePath) {
39
+ try {
40
+ const raw = await fsp.readFile(filePath, "utf8");
41
+ return raw
42
+ .split(/\r?\n/)
43
+ .map((line) => line.trim())
44
+ .filter(Boolean)
45
+ .map((line) => JSON.parse(line));
46
+ } catch (error) {
47
+ if (error && error.code === "ENOENT") return [];
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Zero-dependency JSON state storage.
54
+ */
55
+ export class JsonFileStorage extends StorageProvider {
56
+ constructor({ baseDir = path.resolve(process.cwd(), "output/live-state") } = {}) {
57
+ super();
58
+ this.baseDir = baseDir;
59
+ }
60
+
61
+ namespaceDir(namespace) {
62
+ return path.join(this.baseDir, sanitizeNamespace(namespace));
63
+ }
64
+
65
+ statePath(namespace) {
66
+ return path.join(this.namespaceDir(namespace), "state.json");
67
+ }
68
+
69
+ tradesPath(namespace) {
70
+ return path.join(this.namespaceDir(namespace), "trades.jsonl");
71
+ }
72
+
73
+ equityPath(namespace) {
74
+ return path.join(this.namespaceDir(namespace), "equity.jsonl");
75
+ }
76
+
77
+ async load(namespace) {
78
+ return readJsonFile(this.statePath(namespace));
79
+ }
80
+
81
+ async save(namespace, state) {
82
+ const dir = this.namespaceDir(namespace);
83
+ await ensureDir(dir);
84
+ await writeJsonAtomic(this.statePath(namespace), state);
85
+ }
86
+
87
+ async appendTrade(namespace, trade) {
88
+ await appendJsonLine(this.tradesPath(namespace), trade);
89
+ }
90
+
91
+ async appendEquityPoint(namespace, point) {
92
+ await appendJsonLine(this.equityPath(namespace), point);
93
+ }
94
+
95
+ async loadTrades(namespace) {
96
+ return readJsonLines(this.tradesPath(namespace));
97
+ }
98
+
99
+ async loadEquityCurve(namespace) {
100
+ return readJsonLines(this.equityPath(namespace));
101
+ }
102
+
103
+ async clear(namespace) {
104
+ const dir = this.namespaceDir(namespace);
105
+ if (!fs.existsSync(dir)) return;
106
+ await fsp.rm(dir, { recursive: true, force: true });
107
+ }
108
+ }
109
+
110
+ export function createJsonFileStorage(options) {
111
+ return new JsonFileStorage(options);
112
+ }
@@ -16,11 +16,7 @@ function sortino(values) {
16
16
  const losses = values.filter((value) => value < 0);
17
17
  const downsideDeviation = stddev(losses.length ? losses : [0]);
18
18
  const avg = mean(values);
19
- return downsideDeviation === 0
20
- ? avg > 0
21
- ? Infinity
22
- : 0
23
- : avg / downsideDeviation;
19
+ return downsideDeviation === 0 ? (avg > 0 ? Infinity : 0) : avg / downsideDeviation;
24
20
  }
25
21
 
26
22
  function dayKeyUTC(timeMs) {
@@ -36,10 +32,7 @@ function tradeRMultiple(trade) {
36
32
  const initialRisk = trade._initRisk || 0;
37
33
  if (initialRisk <= 0) return 0;
38
34
  const entry = trade.entryFill ?? trade.entry;
39
- const perUnit =
40
- trade.side === "long"
41
- ? trade.exit.price - entry
42
- : entry - trade.exit.price;
35
+ const perUnit = trade.side === "long" ? trade.exit.price - entry : entry - trade.exit.price;
43
36
  return perUnit / initialRisk;
44
37
  }
45
38
 
@@ -127,6 +120,15 @@ function percentile(values, percentileRank) {
127
120
  return sorted[index];
128
121
  }
129
122
 
123
+ const PROFIT_FACTOR_CAP = 1_000_000;
124
+
125
+ function finiteProfitFactor(grossProfit, grossLoss) {
126
+ if (grossLoss === 0) {
127
+ return grossProfit > 0 ? PROFIT_FACTOR_CAP : 0;
128
+ }
129
+ return grossProfit / grossLoss;
130
+ }
131
+
130
132
  /**
131
133
  * Build aggregate backtest metrics for completed positions and realized trade legs.
132
134
  *
@@ -134,14 +136,7 @@ function percentile(values, percentileRank) {
134
136
  * `profitFactor`, `winRate`, `expectancy`, `maxDrawdown`, `sharpe`, `avgHold`,
135
137
  * and `sideBreakdown`, while preserving the more specific legacy fields.
136
138
  */
137
- export function buildMetrics({
138
- closed,
139
- equityStart,
140
- equityFinal,
141
- candles,
142
- estBarMs,
143
- eqSeries,
144
- }) {
139
+ export function buildMetrics({ closed, equityStart, equityFinal, candles, estBarMs, eqSeries }) {
145
140
  const legs = [...closed].sort((left, right) => left.exit.time - right.exit.time);
146
141
  const completedTrades = [];
147
142
  const tradeRs = [];
@@ -233,18 +228,8 @@ export function buildMetrics({
233
228
  : mean(tradeReturns) / tradeReturnStd;
234
229
  const sortinoPerTrade = sortino(tradeReturns);
235
230
 
236
- const profitFactorPositions =
237
- grossLossPositions === 0
238
- ? grossProfitPositions > 0
239
- ? Infinity
240
- : 0
241
- : grossProfitPositions / grossLossPositions;
242
- const profitFactorLegs =
243
- grossLossLegs === 0
244
- ? grossProfitLegs > 0
245
- ? Infinity
246
- : 0
247
- : grossProfitLegs / grossLossLegs;
231
+ const profitFactorPositions = finiteProfitFactor(grossProfitPositions, grossLossPositions);
232
+ const profitFactorLegs = finiteProfitFactor(grossProfitLegs, grossLossLegs);
248
233
  const returnPct = (equityFinal - equityStart) / Math.max(1e-12, equityStart);
249
234
  const calmar = maxDrawdown === 0 ? (returnPct > 0 ? Infinity : 0) : returnPct / maxDrawdown;
250
235
 
@@ -253,9 +238,7 @@ export function buildMetrics({
253
238
  const avgHoldMin = mean(holdDurationsMinutes);
254
239
 
255
240
  const equitySeries =
256
- eqSeries && eqSeries.length
257
- ? eqSeries
258
- : buildEquitySeriesFromLegs({ legs, equityStart });
241
+ eqSeries && eqSeries.length ? eqSeries : buildEquitySeriesFromLegs({ legs, equityStart });
259
242
  const dailyReturnsSeries = dailyReturns(equitySeries);
260
243
  const dailyStd = stddev(dailyReturnsSeries);
261
244
  const sharpeDaily =
@@ -288,17 +271,13 @@ export function buildMetrics({
288
271
  const sideBreakdown = {
289
272
  long: {
290
273
  trades: longTradesCount,
291
- winRate: longTradesCount
292
- ? longTradeWins / longTradesCount
293
- : 0,
274
+ winRate: longTradesCount ? longTradeWins / longTradesCount : 0,
294
275
  avgPnL: longTradesCount ? longPnLSum / longTradesCount : 0,
295
276
  avgR: mean(longRs),
296
277
  },
297
278
  short: {
298
279
  trades: shortTradesCount,
299
- winRate: shortTradesCount
300
- ? shortTradeWins / shortTradesCount
301
- : 0,
280
+ winRate: shortTradesCount ? shortTradeWins / shortTradesCount : 0,
302
281
  avgPnL: shortTradesCount ? shortPnLSum / shortTradesCount : 0,
303
282
  avgR: mean(shortRs),
304
283
  },
@@ -328,9 +307,7 @@ export function buildMetrics({
328
307
  startEquity: equityStart,
329
308
  profitFactor_pos: profitFactorPositions,
330
309
  profitFactor_leg: profitFactorLegs,
331
- winRate_pos: completedTrades.length
332
- ? winningTradeCount / completedTrades.length
333
- : 0,
310
+ winRate_pos: completedTrades.length ? winningTradeCount / completedTrades.length : 0,
334
311
  winRate_leg: legs.length ? winningLegCount / legs.length : 0,
335
312
  sharpeDaily,
336
313
  sortinoDaily,
@@ -24,10 +24,7 @@ export function exportBacktestArtifacts({
24
24
  metrics: null,
25
25
  };
26
26
 
27
- const csvTrades =
28
- csvSource === "trades"
29
- ? result.trades
30
- : result.positions ?? result.trades;
27
+ const csvTrades = csvSource === "trades" ? result.trades : (result.positions ?? result.trades);
31
28
 
32
29
  if (exportCsv) {
33
30
  outputs.csv = exportTradesCsv(csvTrades, {
@@ -9,10 +9,7 @@ function tradeRMultiple(trade) {
9
9
  const initialRisk = trade._initRisk || 0;
10
10
  if (initialRisk <= 0) return 0;
11
11
  const entry = trade.entryFill ?? trade.entry;
12
- const perUnit =
13
- trade.side === "long"
14
- ? trade.exit.price - entry
15
- : entry - trade.exit.price;
12
+ const perUnit = trade.side === "long" ? trade.exit.price - entry : entry - trade.exit.price;
16
13
  return perUnit / initialRisk;
17
14
  }
18
15
 
@@ -58,9 +55,7 @@ export function exportTradesCsv(
58
55
  (trade.maeR ?? 0).toFixed(3),
59
56
  trade.adds ?? 0,
60
57
  trade.entryATR !== undefined ? Number(trade.entryATR).toFixed(6) : "",
61
- trade.exit.exitATR !== undefined
62
- ? Number(trade.exit.exitATR).toFixed(6)
63
- : "",
58
+ trade.exit.exitATR !== undefined ? Number(trade.exit.exitATR).toFixed(6) : "",
64
59
  ].join(",")
65
60
  ),
66
61
  ].join("\n");
@@ -19,7 +19,8 @@ function candidateRoots() {
19
19
  }
20
20
 
21
21
  function readTemplate(relativePath) {
22
- for (const root of candidateRoots()) {
22
+ const roots = candidateRoots();
23
+ for (const root of roots) {
23
24
  const absolutePath = path.join(root, relativePath);
24
25
  if (!fs.existsSync(absolutePath)) continue;
25
26
 
@@ -29,7 +30,9 @@ function readTemplate(relativePath) {
29
30
  return templateCache.get(absolutePath);
30
31
  }
31
32
 
32
- throw new Error(`Could not locate template asset: ${relativePath}`);
33
+ throw new Error(
34
+ `Could not locate template asset: ${relativePath} (searched ${roots.length} roots starting from ${roots[0]})`
35
+ );
33
36
  }
34
37
 
35
38
  function fmt(value, digits = 2) {
@@ -135,9 +138,7 @@ function renderPositionRows(positions) {
135
138
  <td>${escapeHtml(fmt(exit.price, 4))}</td>
136
139
  <td>${escapeHtml(exit.reason ?? "—")}</td>
137
140
  <td>${escapeHtml(fmt(exit.pnl, 2))}</td>
138
- <td>${escapeHtml(fmt(trade.mfeR ?? 0, 2))} / ${escapeHtml(
139
- fmt(trade.maeR ?? 0, 2)
140
- )}</td>
141
+ <td>${escapeHtml(fmt(trade.mfeR ?? 0, 2))} / ${escapeHtml(fmt(trade.maeR ?? 0, 2))}</td>
141
142
  </tr>
142
143
  `;
143
144
  })
@@ -259,10 +260,7 @@ export function renderHtmlReport({
259
260
  ["R p50 / p90", `${fmt(metrics.rDist?.p50 ?? 0, 2)} / ${fmt(metrics.rDist?.p90 ?? 0, 2)}`],
260
261
  [
261
262
  "Hold p50 / p90",
262
- `${fmt(metrics.holdDistMin?.p50 ?? 0, 1)} / ${fmt(
263
- metrics.holdDistMin?.p90 ?? 0,
264
- 1
265
- )} min`,
263
+ `${fmt(metrics.holdDistMin?.p50 ?? 0, 1)} / ${fmt(metrics.holdDistMin?.p90 ?? 0, 1)} min`,
266
264
  ],
267
265
  ]);
268
266
 
@@ -306,10 +304,7 @@ export function exportHtmlReport({
306
304
  const safeSymbol = String(symbol).replace(/[^a-zA-Z0-9_.-]+/g, "_");
307
305
  const safeInterval = String(interval).replace(/[^a-zA-Z0-9_.-]+/g, "_");
308
306
  const safeRange = String(range).replace(/[^a-zA-Z0-9_.-]+/g, "_");
309
- const outputPath = path.join(
310
- outDir,
311
- `report-${safeSymbol}-${safeInterval}-${safeRange}.html`
312
- );
307
+ const outputPath = path.join(outDir, `report-${safeSymbol}-${safeInterval}-${safeRange}.html`);
313
308
 
314
309
  const html = renderHtmlReport({
315
310
  symbol,
@@ -126,8 +126,7 @@ export function atr(bars, period = 14) {
126
126
  continue;
127
127
  }
128
128
 
129
- previousAtr =
130
- (previousAtr * (period - 1) + trueRanges[index]) / period;
129
+ previousAtr = (previousAtr * (period - 1) + trueRanges[index]) / period;
131
130
  output[index] = previousAtr;
132
131
  }
133
132
 
@@ -2,6 +2,16 @@ function roundStep(value, step) {
2
2
  return Math.floor(value / step) * step;
3
3
  }
4
4
 
5
+ let warnedNonPositiveEquity = false;
6
+
7
+ function warnNonPositiveEquity(equity) {
8
+ if (warnedNonPositiveEquity) return;
9
+ warnedNonPositiveEquity = true;
10
+ console.warn(
11
+ `[tradelab] calculatePositionSize() received non-positive equity (${equity}); returning size 0`
12
+ );
13
+ }
14
+
5
15
  export function calculatePositionSize({
6
16
  equity,
7
17
  entry,
@@ -11,14 +21,18 @@ export function calculatePositionSize({
11
21
  minQty = 0.001,
12
22
  maxLeverage = 2,
13
23
  }) {
24
+ if (!Number.isFinite(equity) || equity <= 0) {
25
+ warnNonPositiveEquity(equity);
26
+ return 0;
27
+ }
28
+
14
29
  const riskPerUnit = Math.abs(entry - stop);
15
30
  if (!Number.isFinite(riskPerUnit) || riskPerUnit <= 0) return 0;
16
31
 
17
32
  const maxRiskDollars = Math.max(0, equity * riskFraction);
18
33
  let quantity = maxRiskDollars / riskPerUnit;
19
34
 
20
- const leverageCapQty =
21
- (equity * maxLeverage) / Math.max(1e-12, Math.abs(entry));
35
+ const leverageCapQty = (equity * maxLeverage) / Math.max(1e-12, Math.abs(entry));
22
36
  quantity = Math.min(quantity, leverageCapQty);
23
37
  quantity = roundStep(quantity, qtyStep);
24
38
 
package/src/utils/time.js CHANGED
@@ -8,20 +8,14 @@ function usDstBoundsUTC(year) {
8
8
  marchCursor = new Date(marchCursor.getTime() + 24 * 60 * 60 * 1000);
9
9
  }
10
10
 
11
- const dstStart = new Date(
12
- Date.UTC(year, 2, marchCursor.getUTCDate(), 7, 0, 0)
13
- );
11
+ const dstStart = new Date(Date.UTC(year, 2, marchCursor.getUTCDate(), 7, 0, 0));
14
12
 
15
13
  let novemberCursor = new Date(Date.UTC(year, 10, 1, 6, 0, 0));
16
14
  while (novemberCursor.getUTCDay() !== 0) {
17
- novemberCursor = new Date(
18
- novemberCursor.getTime() + 24 * 60 * 60 * 1000
19
- );
15
+ novemberCursor = new Date(novemberCursor.getTime() + 24 * 60 * 60 * 1000);
20
16
  }
21
17
 
22
- const dstEnd = new Date(
23
- Date.UTC(year, 10, novemberCursor.getUTCDate(), 6, 0, 0)
24
- );
18
+ const dstEnd = new Date(Date.UTC(year, 10, novemberCursor.getUTCDate(), 6, 0, 0));
25
19
 
26
20
  return { dstStart, dstEnd };
27
21
  }
@@ -58,9 +52,7 @@ export function isSession(timeMs, session = "NYSE") {
58
52
  if (session === "FUT") {
59
53
  const maintenanceStart = 17 * 60;
60
54
  const maintenanceEnd = 18 * 60;
61
- return !(
62
- minutes >= maintenanceStart && minutes < maintenanceEnd
63
- );
55
+ return !(minutes >= maintenanceStart && minutes < maintenanceEnd);
64
56
  }
65
57
 
66
58
  const open = 9 * 60 + 30;
@@ -4,7 +4,9 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>{{TITLE}} backtest</title>
7
- <style>{{CSS}}</style>
7
+ <style>
8
+ {{CSS}}
9
+ </style>
8
10
  </head>
9
11
  <body>
10
12
  <main class="report-shell">
@@ -17,9 +19,7 @@
17
19
  <div class="hero-pill">{{HERO_PILL}}</div>
18
20
  </header>
19
21
 
20
- <section class="metric-grid">
21
- {{METRIC_CARDS}}
22
- </section>
22
+ <section class="metric-grid">{{METRIC_CARDS}}</section>
23
23
 
24
24
  <section class="panel-grid">
25
25
  <article class="panel panel--wide">
@@ -36,7 +36,9 @@
36
36
  <p>Headline stats for completed positions.</p>
37
37
  </div>
38
38
  <table class="data-table">
39
- <tbody>{{SUMMARY_ROWS}}</tbody>
39
+ <tbody>
40
+ {{SUMMARY_ROWS}}
41
+ </tbody>
40
42
  </table>
41
43
  </article>
42
44
 
@@ -70,7 +72,9 @@
70
72
  <p>Long/short split and distribution snapshots.</p>
71
73
  </div>
72
74
  <table class="data-table">
73
- <tbody>{{BREAKDOWN_ROWS}}</tbody>
75
+ <tbody>
76
+ {{BREAKDOWN_ROWS}}
77
+ </tbody>
74
78
  </table>
75
79
  </article>
76
80
 
@@ -92,15 +96,25 @@
92
96
  <th>MFE / MAE</th>
93
97
  </tr>
94
98
  </thead>
95
- <tbody>{{POSITION_ROWS}}</tbody>
99
+ <tbody>
100
+ {{POSITION_ROWS}}
101
+ </tbody>
96
102
  </table>
97
103
  </div>
98
104
  </article>
99
105
  </section>
100
106
  </main>
101
107
 
102
- <script id="report-data" type="application/json">{{REPORT_DATA_JSON}}</script>
108
+ <script id="report-data" type="application/json">
109
+ {{REPORT_DATA_JSON}}
110
+ </script>
103
111
  <script src="{{PLOTLY_CDN_URL}}"></script>
104
- <script>{{REPORT_JS}}</script>
112
+ <script>
113
+ {
114
+ {
115
+ REPORT_JS;
116
+ }
117
+ }
118
+ </script>
105
119
  </body>
106
120
  </html>
@@ -34,87 +34,101 @@
34
34
  );
35
35
  }
36
36
 
37
- plot("equity-chart", [
37
+ plot(
38
+ "equity-chart",
39
+ [
40
+ {
41
+ x: data.eqSeries.map((point) => point.t),
42
+ y: data.eqSeries.map((point) => point.equity),
43
+ type: "scatter",
44
+ mode: "lines",
45
+ name: "Equity",
46
+ line: { color: "#64e0c1", width: 2.5 },
47
+ },
48
+ ],
38
49
  {
39
- x: data.eqSeries.map((point) => point.t),
40
- y: data.eqSeries.map((point) => point.equity),
41
- type: "scatter",
42
- mode: "lines",
43
- name: "Equity",
44
- line: { color: "#64e0c1", width: 2.5 },
45
- },
46
- ], {
47
- yaxis: { title: "Equity", gridcolor: "rgba(159,176,201,0.12)" },
48
- });
50
+ yaxis: { title: "Equity", gridcolor: "rgba(159,176,201,0.12)" },
51
+ }
52
+ );
49
53
 
50
- plot("drawdown-chart", [
54
+ plot(
55
+ "drawdown-chart",
56
+ [
57
+ {
58
+ x: data.drawdown.map((point) => point.t),
59
+ y: data.drawdown.map((point) => point.value),
60
+ type: "scatter",
61
+ mode: "lines",
62
+ name: "Drawdown",
63
+ line: { color: "#fb7185", width: 2.2 },
64
+ fill: "tozeroy",
65
+ fillcolor: "rgba(251,113,133,0.12)",
66
+ },
67
+ ],
51
68
  {
52
- x: data.drawdown.map((point) => point.t),
53
- y: data.drawdown.map((point) => point.value),
54
- type: "scatter",
55
- mode: "lines",
56
- name: "Drawdown",
57
- line: { color: "#fb7185", width: 2.2 },
58
- fill: "tozeroy",
59
- fillcolor: "rgba(251,113,133,0.12)",
60
- },
61
- ], {
62
- yaxis: {
63
- title: "Drawdown",
64
- tickformat: ",.0%",
65
- gridcolor: "rgba(159,176,201,0.12)",
66
- },
67
- });
69
+ yaxis: {
70
+ title: "Drawdown",
71
+ tickformat: ",.0%",
72
+ gridcolor: "rgba(159,176,201,0.12)",
73
+ },
74
+ }
75
+ );
68
76
 
69
77
  if (Array.isArray(data.dailyPnl) && data.dailyPnl.length) {
70
- plot("daily-chart", [
71
- {
72
- x: data.dailyPnl.map((point) => point.date),
73
- y: data.dailyPnl.map((point) => point.pnl),
74
- type: "bar",
75
- name: "Daily PnL",
76
- marker: {
77
- color: data.dailyPnl.map((point) =>
78
- point.pnl >= 0 ? "#4ade80" : "#fb7185"
79
- ),
78
+ plot(
79
+ "daily-chart",
80
+ [
81
+ {
82
+ x: data.dailyPnl.map((point) => point.date),
83
+ y: data.dailyPnl.map((point) => point.pnl),
84
+ type: "bar",
85
+ name: "Daily PnL",
86
+ marker: {
87
+ color: data.dailyPnl.map((point) => (point.pnl >= 0 ? "#4ade80" : "#fb7185")),
88
+ },
80
89
  },
81
- },
82
- ], {
83
- yaxis: { title: "PnL", gridcolor: "rgba(159,176,201,0.12)" },
84
- });
90
+ ],
91
+ {
92
+ yaxis: { title: "PnL", gridcolor: "rgba(159,176,201,0.12)" },
93
+ }
94
+ );
85
95
  }
86
96
 
87
97
  if (data.hasReplay && Array.isArray(data.replay.frames)) {
88
98
  const entries = data.replay.events.filter((event) => event.type === "entry");
89
99
  const exits = data.replay.events.filter((event) => event.type !== "entry");
90
100
 
91
- plot("replay-chart", [
92
- {
93
- x: data.replay.frames.map((frame) => frame.t),
94
- y: data.replay.frames.map((frame) => frame.price),
95
- type: "scatter",
96
- mode: "lines",
97
- name: "Price",
98
- line: { color: "#93c5fd", width: 2 },
99
- },
100
- {
101
- x: entries.map((event) => event.t),
102
- y: entries.map((event) => event.price),
103
- type: "scatter",
104
- mode: "markers",
105
- name: "Entries",
106
- marker: { color: "#4ade80", size: 9, symbol: "triangle-up" },
107
- },
101
+ plot(
102
+ "replay-chart",
103
+ [
104
+ {
105
+ x: data.replay.frames.map((frame) => frame.t),
106
+ y: data.replay.frames.map((frame) => frame.price),
107
+ type: "scatter",
108
+ mode: "lines",
109
+ name: "Price",
110
+ line: { color: "#93c5fd", width: 2 },
111
+ },
112
+ {
113
+ x: entries.map((event) => event.t),
114
+ y: entries.map((event) => event.price),
115
+ type: "scatter",
116
+ mode: "markers",
117
+ name: "Entries",
118
+ marker: { color: "#4ade80", size: 9, symbol: "triangle-up" },
119
+ },
120
+ {
121
+ x: exits.map((event) => event.t),
122
+ y: exits.map((event) => event.price),
123
+ type: "scatter",
124
+ mode: "markers",
125
+ name: "Exits",
126
+ marker: { color: "#fb7185", size: 9, symbol: "triangle-down" },
127
+ },
128
+ ],
108
129
  {
109
- x: exits.map((event) => event.t),
110
- y: exits.map((event) => event.price),
111
- type: "scatter",
112
- mode: "markers",
113
- name: "Exits",
114
- marker: { color: "#fb7185", size: 9, symbol: "triangle-down" },
115
- },
116
- ], {
117
- yaxis: { title: "Price", gridcolor: "rgba(159,176,201,0.12)" },
118
- });
130
+ yaxis: { title: "Price", gridcolor: "rgba(159,176,201,0.12)" },
131
+ }
132
+ );
119
133
  }
120
134
  })();