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.
- package/README.md +121 -52
- package/bin/tradelab.js +340 -49
- package/dist/cjs/data.cjs +210 -155
- package/dist/cjs/index.cjs +1782 -274
- package/dist/cjs/live.cjs +3350 -0
- package/docs/README.md +26 -9
- package/docs/api-reference.md +89 -26
- package/docs/backtest-engine.md +74 -60
- package/docs/data-reporting-cli.md +66 -36
- package/docs/examples.md +275 -0
- package/docs/live-trading.md +186 -0
- package/examples/yahooEmaCross.js +1 -6
- package/package.json +18 -3
- package/src/data/csv.js +24 -14
- package/src/data/index.js +1 -5
- package/src/data/yahoo.js +6 -19
- package/src/engine/backtest.js +137 -144
- package/src/engine/backtestTicks.js +481 -0
- package/src/engine/barSystemRunner.js +1027 -0
- package/src/engine/execution.js +11 -39
- package/src/engine/portfolio.js +237 -66
- package/src/engine/walkForward.js +132 -13
- package/src/index.js +3 -11
- package/src/live/broker/alpaca.js +254 -0
- package/src/live/broker/binance.js +351 -0
- package/src/live/broker/coinbase.js +339 -0
- package/src/live/broker/interactiveBrokers.js +123 -0
- package/src/live/broker/interface.js +74 -0
- package/src/live/clock.js +56 -0
- package/src/live/engine/candleAggregator.js +154 -0
- package/src/live/engine/liveEngine.js +694 -0
- package/src/live/engine/paperEngine.js +453 -0
- package/src/live/engine/riskManager.js +185 -0
- package/src/live/engine/stateManager.js +112 -0
- package/src/live/events.js +48 -0
- package/src/live/feed/brokerFeed.js +35 -0
- package/src/live/feed/interface.js +28 -0
- package/src/live/feed/pollingFeed.js +105 -0
- package/src/live/index.js +27 -0
- package/src/live/logger.js +82 -0
- package/src/live/orchestrator.js +133 -0
- package/src/live/storage/interface.js +36 -0
- package/src/live/storage/jsonFileStorage.js +112 -0
- package/src/metrics/buildMetrics.js +103 -100
- package/src/reporting/exportBacktestArtifacts.js +1 -4
- package/src/reporting/exportTradesCsv.js +2 -7
- package/src/reporting/renderHtmlReport.js +8 -13
- package/src/utils/indicators.js +1 -2
- package/src/utils/positionSizing.js +16 -2
- package/src/utils/time.js +4 -12
- package/templates/report.html +23 -9
- package/templates/report.js +83 -69
- package/types/index.d.ts +98 -4
- package/types/live.d.ts +382 -0
package/templates/report.js
CHANGED
|
@@ -34,87 +34,101 @@
|
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
plot(
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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:
|
|
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 }>
|