tradelab 0.4.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 +121 -52
  2. package/bin/tradelab.js +340 -49
  3. package/dist/cjs/data.cjs +210 -155
  4. package/dist/cjs/index.cjs +1782 -274
  5. package/dist/cjs/live.cjs +3350 -0
  6. package/docs/README.md +26 -9
  7. package/docs/api-reference.md +89 -26
  8. package/docs/backtest-engine.md +74 -60
  9. package/docs/data-reporting-cli.md +66 -36
  10. package/docs/examples.md +275 -0
  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 +481 -0
  19. package/src/engine/barSystemRunner.js +1027 -0
  20. package/src/engine/execution.js +11 -39
  21. package/src/engine/portfolio.js +237 -66
  22. package/src/engine/walkForward.js +132 -13
  23. package/src/index.js +3 -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 +103 -100
  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 +98 -4
  54. package/types/live.d.ts +382 -0
@@ -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
  })();
package/types/index.d.ts CHANGED
@@ -10,6 +10,20 @@ export interface Candle {
10
10
  [key: string]: unknown;
11
11
  }
12
12
 
13
+ export interface Tick {
14
+ time: number;
15
+ price?: number;
16
+ last?: number;
17
+ bid?: number;
18
+ ask?: number;
19
+ high?: number;
20
+ low?: number;
21
+ close?: number;
22
+ size?: number;
23
+ volume?: number;
24
+ [key: string]: unknown;
25
+ }
26
+
13
27
  /** Realized equity snapshot captured during a backtest. */
14
28
  export interface EquityPoint {
15
29
  /** Bar timestamp in Unix milliseconds. */
@@ -18,6 +32,10 @@ export interface EquityPoint {
18
32
  timestamp: number;
19
33
  /** Realized account equity at this point in the run. */
20
34
  equity: number;
35
+ /** Capital currently locked by open positions, when available. */
36
+ lockedCapital?: number;
37
+ /** Capital currently available for new positions, when available. */
38
+ availableCapital?: number;
21
39
  }
22
40
 
23
41
  /** Lightweight chart frame for replay/export consumers. */
@@ -32,6 +50,8 @@ export interface ReplayFrame {
32
50
  posSide: Side | null;
33
51
  /** Active position size at the frame time. */
34
52
  posSize: number;
53
+ lockedCapital?: number;
54
+ availableCapital?: number;
35
55
  }
36
56
 
37
57
  /** Replay event emitted for entries, exits, adds, and scale-outs. */
@@ -88,6 +108,22 @@ export interface BacktestTrade {
88
108
  [key: string]: unknown;
89
109
  }
90
110
 
111
+ export interface OpenPosition {
112
+ id?: number;
113
+ symbol?: string;
114
+ side: Side;
115
+ entry: number;
116
+ entryFill?: number;
117
+ stop: number;
118
+ takeProfit: number;
119
+ size: number;
120
+ openTime: number;
121
+ markPrice: number;
122
+ unrealizedPnl: number;
123
+ _initRisk?: number;
124
+ [key: string]: unknown;
125
+ }
126
+
91
127
  export interface SideBreakdownEntry {
92
128
  trades: number;
93
129
  winRate: number;
@@ -301,6 +337,29 @@ export interface BacktestOptions {
301
337
  strict?: boolean;
302
338
  }
303
339
 
340
+ export interface BacktestTickOptions {
341
+ ticks: Tick[];
342
+ symbol?: string;
343
+ equity?: number;
344
+ riskPct?: number;
345
+ signal: SignalFunction;
346
+ interval?: string;
347
+ range?: string;
348
+ slippageBps?: number;
349
+ feeBps?: number;
350
+ costs?: ExecutionCostOptions;
351
+ finalTP_R?: number;
352
+ maxDailyLossPct?: number;
353
+ dailyMaxTrades?: number;
354
+ qtyStep?: number;
355
+ minQty?: number;
356
+ maxLeverage?: number;
357
+ collectEqSeries?: boolean;
358
+ collectReplay?: boolean;
359
+ queueFillProbability?: number;
360
+ oco?: OCOOptions;
361
+ }
362
+
304
363
  /** Full result payload returned by `backtest()`. */
305
364
  export interface BacktestResult {
306
365
  symbol?: string;
@@ -310,6 +369,8 @@ export interface BacktestResult {
310
369
  trades: BacktestTrade[];
311
370
  /** Completed positions only, without intermediate realized legs. */
312
371
  positions: BacktestTrade[];
372
+ /** Open positions still active at end-of-data (if any). */
373
+ openPositions: OpenPosition[];
313
374
  /** Aggregate performance statistics. */
314
375
  metrics: BacktestMetrics;
315
376
  /** Realized equity points suitable for charts and exports. */
@@ -320,12 +381,16 @@ export interface BacktestResult {
320
381
 
321
382
  export interface PortfolioSystem extends Omit<BacktestOptions, "equity"> {
322
383
  weight?: number;
384
+ maxAllocation?: number;
385
+ maxAllocationPct?: number;
323
386
  }
324
387
 
325
388
  export interface PortfolioSystemResult {
326
389
  symbol: string;
327
390
  weight: number;
328
391
  equity: number;
392
+ allocationCapPct?: number;
393
+ allocationCap?: number;
329
394
  result: BacktestResult;
330
395
  }
331
396
 
@@ -340,12 +405,38 @@ export interface WalkForwardWindow {
340
405
  trainScore: number;
341
406
  trainMetrics: BacktestMetrics;
342
407
  testMetrics: BacktestMetrics;
408
+ oosTrades: number;
409
+ profitable: boolean;
410
+ stabilityScore: number;
343
411
  result: BacktestResult;
344
412
  }
345
413
 
414
+ export interface WalkForwardBestParamsSummary {
415
+ adjacentRepeatRate: number;
416
+ uniqueWinnerCount: number;
417
+ dominant: {
418
+ params: Record<string, unknown>;
419
+ wins: number;
420
+ profitableWindows: number;
421
+ oosTrades: number;
422
+ } | null;
423
+ leaderboard: Array<{
424
+ params: Record<string, unknown>;
425
+ wins: number;
426
+ profitableWindows: number;
427
+ oosTrades: number;
428
+ }>;
429
+ }
430
+
431
+ export interface WalkForwardBestParams extends Array<Record<string, unknown>> {
432
+ winners: Array<Record<string, unknown>>;
433
+ stability: WalkForwardBestParamsSummary;
434
+ }
435
+
346
436
  export interface WalkForwardResult extends BacktestResult {
347
437
  windows: WalkForwardWindow[];
348
- bestParams: Array<Record<string, unknown>>;
438
+ bestParams: WalkForwardBestParams;
439
+ bestParamsSummary: WalkForwardBestParamsSummary;
349
440
  }
350
441
 
351
442
  export interface CsvLoadOptions {
@@ -459,12 +550,16 @@ export interface ArtifactPaths {
459
550
  * chart-friendly replay frames/events in `replay`.
460
551
  */
461
552
  export function backtest(options: BacktestOptions): BacktestResult;
553
+ export function backtestTicks(options: BacktestTickOptions): BacktestResult;
462
554
  export function backtestPortfolio(options: {
463
555
  systems: PortfolioSystem[];
464
556
  equity?: number;
465
557
  allocation?: "equal" | "weight";
466
558
  collectEqSeries?: boolean;
467
559
  collectReplay?: boolean;
560
+ maxDailyLossPct?: number;
561
+ processingOrder?: "sequential" | "shuffle";
562
+ shuffleSeed?: number;
468
563
  }): PortfolioBacktestResult;
469
564
  export function walkForwardOptimize(options: {
470
565
  candles: Candle[];
@@ -473,6 +568,7 @@ export function walkForwardOptimize(options: {
473
568
  trainBars: number;
474
569
  testBars: number;
475
570
  stepBars?: number;
571
+ mode?: "rolling" | "anchored";
476
572
  scoreBy?: keyof BacktestMetrics;
477
573
  backtestOptions?: Omit<BacktestOptions, "candles" | "signal">;
478
574
  }): WalkForwardResult;
@@ -561,9 +657,7 @@ export function calculatePositionSize(input: {
561
657
  export function offsetET(timeMs: number): number;
562
658
  export function minutesET(timeMs: number): number;
563
659
  export function isSession(timeMs: number, session?: "NYSE" | "FUT" | "AUTO"): boolean;
564
- export function parseWindowsCSV(
565
- csv: string
566
- ): Array<{ aMin: number; bMin: number }> | null;
660
+ export function parseWindowsCSV(csv: string): Array<{ aMin: number; bMin: number }> | null;
567
661
  export function inWindowsET(
568
662
  timeMs: number,
569
663
  windows: Array<{ aMin: number; bMin: number }>