tradelab 0.2.0 → 0.4.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 +273 -121
- package/bin/tradelab.js +384 -0
- package/dist/cjs/data.cjs +59 -27
- package/dist/cjs/index.cjs +291 -29
- package/docs/README.md +61 -0
- package/docs/api-reference.md +70 -0
- package/docs/backtest-engine.md +363 -0
- package/docs/data-reporting-cli.md +254 -0
- package/package.json +6 -1
- package/src/engine/backtest.js +29 -20
- package/src/engine/execution.js +44 -7
- package/src/engine/portfolio.js +160 -0
- package/src/engine/walkForward.js +126 -0
- package/src/index.js +2 -0
- package/types/index.d.ts +58 -0
package/README.md
CHANGED
|
@@ -1,52 +1,61 @@
|
|
|
1
|
-
<
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://i.imgur.com/HGvvQbq.png" width="420" alt="tradelab logo" />
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
<p><strong>A Node.js backtesting toolkit for serious trading strategy research.</strong></p>
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
-
|
|
6
|
+
[](https://www.npmjs.com/package/tradelab)
|
|
7
|
+
[](https://github.com/ishsharm0/tradelab)
|
|
8
|
+
[](https://github.com/ishsharm0/tradelab/blob/main/LICENSE)
|
|
9
|
+
[](https://nodejs.org)
|
|
10
|
+
[](https://github.com/ishsharm0/tradelab/blob/main/types/index.d.ts)
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
</div>
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
---
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
**tradelab** handles the simulation, sizing, exits, costs, and result exports — you bring the candles and signal logic.
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
- Backtest engine with pending entries, OCO exits, scale-outs, pyramiding, cooldowns, daily loss limits, and optional replay/equity capture
|
|
18
|
-
- Historical data loading from Yahoo Finance, with local caching to avoid repeated downloads
|
|
19
|
-
- CSV import for common OHLCV formats and custom column mappings
|
|
20
|
-
- Position-level and leg-level metrics, including drawdown, expectancy, hold-time stats, and side breakdowns
|
|
21
|
-
- HTML report export, metrics JSON export, and trade CSV export
|
|
22
|
-
- Utility indicators and session helpers for strategy development
|
|
23
|
-
- TypeScript definitions for the public API
|
|
24
|
-
|
|
25
|
-
## Installation
|
|
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.
|
|
26
19
|
|
|
27
20
|
```bash
|
|
28
21
|
npm install tradelab
|
|
29
22
|
```
|
|
30
23
|
|
|
31
|
-
|
|
24
|
+
---
|
|
32
25
|
|
|
33
|
-
##
|
|
26
|
+
## Table of contents
|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
- [What it includes](#what-it-includes)
|
|
29
|
+
- [Quick start](#quick-start)
|
|
30
|
+
- [Loading historical data](#loading-historical-data)
|
|
31
|
+
- [Core concepts](#core-concepts)
|
|
32
|
+
- [Portfolio mode](#portfolio-mode)
|
|
33
|
+
- [Walk-forward optimization](#walk-forward-optimization)
|
|
34
|
+
- [Execution and cost modeling](#execution-and-cost-modeling)
|
|
35
|
+
- [Exports and reporting](#exports-and-reporting)
|
|
36
|
+
- [CLI](#cli)
|
|
37
|
+
- [Examples](#examples)
|
|
38
|
+
- [Documentation](#documentation)
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
const { backtest, getHistoricalCandles, ema } = require("tradelab");
|
|
39
|
-
const { fetchHistorical } = require("tradelab/data");
|
|
40
|
-
```
|
|
40
|
+
---
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
## What it includes
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
| Area | What you get |
|
|
45
|
+
|---|---|
|
|
46
|
+
| **Engine** | Candle-based backtests with position sizing, exits, risk controls, replay capture |
|
|
47
|
+
| **Portfolio** | Multi-symbol aggregation with weight-based capital allocation |
|
|
48
|
+
| **Walk-forward** | Rolling train/test validation with parameter search |
|
|
49
|
+
| **Data** | Yahoo Finance downloads, CSV import, and local cache helpers |
|
|
50
|
+
| **Costs** | Slippage, spread, and commission modeling |
|
|
51
|
+
| **Exports** | HTML reports, metrics JSON, and trade CSV |
|
|
52
|
+
| **Dev experience** | TypeScript definitions, ESM/CJS support, CLI for quick runs |
|
|
53
|
+
|
|
54
|
+
---
|
|
48
55
|
|
|
49
|
-
## Quick
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
If you already have candles, `backtest()` is the main entry point.
|
|
50
59
|
|
|
51
60
|
```js
|
|
52
61
|
import { backtest, ema, exportBacktestArtifacts } from "tradelab";
|
|
@@ -72,29 +81,23 @@ const result = backtest({
|
|
|
72
81
|
const risk = entry - stop;
|
|
73
82
|
if (risk <= 0) return null;
|
|
74
83
|
|
|
75
|
-
return {
|
|
76
|
-
side: "long",
|
|
77
|
-
entry,
|
|
78
|
-
stop,
|
|
79
|
-
rr: 2,
|
|
80
|
-
};
|
|
84
|
+
return { side: "long", entry, stop, rr: 2 };
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
return null;
|
|
84
88
|
},
|
|
85
89
|
});
|
|
86
90
|
|
|
87
|
-
exportBacktestArtifacts({
|
|
88
|
-
result,
|
|
89
|
-
outDir: "./output",
|
|
90
|
-
});
|
|
91
|
+
exportBacktestArtifacts({ result, outDir: "./output" });
|
|
91
92
|
```
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
After the run, check `result.metrics` for the headline numbers and `result.positions` for the trade log.
|
|
95
|
+
|
|
96
|
+
---
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
## Loading historical data
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
Most users can start with `getHistoricalCandles()`. It abstracts over Yahoo Finance and CSV, handles caching, and normalizes the output so it feeds straight into `backtest()`.
|
|
98
101
|
|
|
99
102
|
```js
|
|
100
103
|
import { getHistoricalCandles, backtest } from "tradelab";
|
|
@@ -104,117 +107,224 @@ const candles = await getHistoricalCandles({
|
|
|
104
107
|
symbol: "SPY",
|
|
105
108
|
interval: "1d",
|
|
106
109
|
period: "2y",
|
|
107
|
-
cache: true,
|
|
110
|
+
cache: true, // reuses local copy on repeated runs
|
|
108
111
|
});
|
|
109
112
|
|
|
110
|
-
const result = backtest({
|
|
111
|
-
candles,
|
|
112
|
-
symbol: "SPY",
|
|
113
|
-
interval: "1d",
|
|
114
|
-
range: "2y",
|
|
115
|
-
signal,
|
|
116
|
-
});
|
|
113
|
+
const result = backtest({ candles, symbol: "SPY", interval: "1d", range: "2y", signal });
|
|
117
114
|
```
|
|
118
115
|
|
|
119
|
-
Supported
|
|
116
|
+
**Supported sources:** `yahoo` · `csv` · `auto`
|
|
120
117
|
|
|
121
|
-
|
|
118
|
+
**Supported periods:** `5d` · `60d` · `6mo` · `1y` · `2y` · and more
|
|
122
119
|
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
Use `cache: true` for repeatable research runs. It eliminates network noise and makes failures easier to diagnose.
|
|
121
|
+
|
|
122
|
+
### CSV import
|
|
125
123
|
|
|
124
|
+
```js
|
|
126
125
|
const candles = await getHistoricalCandles({
|
|
127
126
|
source: "csv",
|
|
128
|
-
|
|
129
|
-
interval: "5m",
|
|
130
|
-
csvPath: "./data/btc-5m.csv",
|
|
127
|
+
csvPath: "./data/spy.csv",
|
|
131
128
|
csv: {
|
|
132
|
-
timeCol: "
|
|
129
|
+
timeCol: "timestamp",
|
|
133
130
|
openCol: "open",
|
|
134
|
-
|
|
135
|
-
lowCol: "low",
|
|
136
|
-
closeCol: "close",
|
|
137
|
-
volumeCol: "volume",
|
|
131
|
+
// ... optional column mapping
|
|
138
132
|
},
|
|
139
133
|
});
|
|
140
134
|
```
|
|
141
135
|
|
|
142
|
-
If
|
|
136
|
+
If your CSV already uses standard OHLCV column names, no mapping is needed at all.
|
|
137
|
+
|
|
138
|
+
---
|
|
143
139
|
|
|
144
|
-
##
|
|
140
|
+
## Core concepts
|
|
145
141
|
|
|
146
|
-
|
|
142
|
+
### The signal function
|
|
143
|
+
|
|
144
|
+
Your signal function is called on every bar. Return `null` to skip, or a signal object to open a trade.
|
|
147
145
|
|
|
148
146
|
```js
|
|
149
|
-
{
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
147
|
+
signal({ candles, index, bar, equity, openPosition, pendingOrder }) {
|
|
148
|
+
// return null to skip
|
|
149
|
+
// return a signal to enter
|
|
150
|
+
return {
|
|
151
|
+
side: "long", // "long" | "short" | "buy" | "sell"
|
|
152
|
+
entry: bar.close, // defaults to current close if omitted
|
|
153
|
+
stop: bar.close - 2,
|
|
154
|
+
rr: 2, // target = entry + (entry - stop) * rr
|
|
155
|
+
};
|
|
156
156
|
}
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
The minimum viable signal is just `side`, `stop`, and `rr`. Start there and add fields only when the strategy actually needs them.
|
|
160
|
+
|
|
161
|
+
### Key backtest options
|
|
162
|
+
|
|
163
|
+
| Option | Purpose |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `equity` | Starting equity (default `10000`) |
|
|
166
|
+
| `riskPct` | Percent of equity risked per trade |
|
|
167
|
+
| `warmupBars` | Bars skipped before signal evaluation starts |
|
|
168
|
+
| `flattenAtClose` | Forces end-of-day exit when enabled |
|
|
169
|
+
| `costs` | Slippage, spread, and commission model |
|
|
170
|
+
| `strict` | Throws on lookahead access |
|
|
171
|
+
| `collectEqSeries` | Enables equity curve output |
|
|
172
|
+
| `collectReplay` | Enables visualization payload |
|
|
173
|
+
|
|
174
|
+
### Result shape
|
|
160
175
|
|
|
161
176
|
```js
|
|
162
177
|
{
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
symbol, interval, range,
|
|
179
|
+
trades, // every realized leg, including partial exits
|
|
180
|
+
positions, // completed positions — start here for analysis
|
|
181
|
+
metrics, // winRate, profitFactor, maxDrawdown, sharpe, ...
|
|
182
|
+
eqSeries, // [{ time, timestamp, equity }] — equity curve
|
|
183
|
+
replay, // visualization frames and events
|
|
167
184
|
}
|
|
168
185
|
```
|
|
169
186
|
|
|
170
|
-
|
|
187
|
+
**First checks after any run:**
|
|
188
|
+
|
|
189
|
+
- `metrics.trades` — enough sample size to trust the numbers?
|
|
190
|
+
- `metrics.profitFactor` — do winners beat losers gross of costs?
|
|
191
|
+
- `metrics.maxDrawdown` — is the equity path survivable?
|
|
192
|
+
- `metrics.sideBreakdown` — does one side carry the whole result?
|
|
171
193
|
|
|
172
|
-
|
|
173
|
-
- `entry` can be omitted and will default to the current bar close
|
|
174
|
-
- `takeProfit` can be omitted if `rr` or `_rr` is provided
|
|
175
|
-
- `qty` or `size` can override risk-based sizing
|
|
176
|
-
- `riskPct` or `riskFraction` can override the global risk setting per signal
|
|
177
|
-
- `strict: true` throws if the strategy directly accesses candles beyond the current index
|
|
194
|
+
---
|
|
178
195
|
|
|
179
|
-
|
|
196
|
+
## Portfolio mode
|
|
197
|
+
|
|
198
|
+
Use `backtestPortfolio()` when you have one candle array per symbol and want a single combined result.
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
import { backtestPortfolio } from "tradelab";
|
|
202
|
+
|
|
203
|
+
const result = backtestPortfolio({
|
|
204
|
+
equity: 100_000,
|
|
205
|
+
systems: [
|
|
206
|
+
{ symbol: "SPY", candles: spy, signal: signalA, weight: 2 },
|
|
207
|
+
{ symbol: "QQQ", candles: qqq, signal: signalB, weight: 1 },
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
```
|
|
180
211
|
|
|
181
|
-
-
|
|
182
|
-
- `_cooldownBars`
|
|
183
|
-
- `_breakevenAtR`
|
|
184
|
-
- `_trailAfterR`
|
|
185
|
-
- `_maxBarsInTrade`
|
|
186
|
-
- `_maxHoldMin`
|
|
187
|
-
- `_rr`
|
|
188
|
-
- `_initRisk`
|
|
189
|
-
- `_imb`
|
|
212
|
+
Capital is allocated up front by weight. Each system runs through the normal single-symbol engine, and the portfolio result merges trades, positions, replay events, and the equity series.
|
|
190
213
|
|
|
191
|
-
|
|
214
|
+
---
|
|
192
215
|
|
|
193
|
-
|
|
216
|
+
## Walk-forward optimization
|
|
194
217
|
|
|
195
|
-
|
|
196
|
-
- `positions`: completed positions only
|
|
197
|
-
- `metrics`: aggregate stats including `winRate`, `expectancy`, `profitFactor`, `maxDrawdown`, `sharpe`, `avgHold`, and `sideBreakdown`
|
|
198
|
-
- `eqSeries`: realized equity history as `{ time, timestamp, equity }`
|
|
199
|
-
- `replay`: chart-friendly frame and event data
|
|
218
|
+
Use `walkForwardOptimize()` when one in-sample backtest is not enough. It runs rolling train/test windows across the full candle history.
|
|
200
219
|
|
|
201
|
-
|
|
220
|
+
```js
|
|
221
|
+
import { walkForwardOptimize } from "tradelab";
|
|
202
222
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
const wf = walkForwardOptimize({
|
|
224
|
+
candles,
|
|
225
|
+
trainBars: 180,
|
|
226
|
+
testBars: 60,
|
|
227
|
+
stepBars: 60,
|
|
228
|
+
scoreBy: "profitFactor",
|
|
229
|
+
parameterSets: [
|
|
230
|
+
{ fast: 8, slow: 21, rr: 2 },
|
|
231
|
+
{ fast: 10, slow: 30, rr: 2 },
|
|
232
|
+
],
|
|
233
|
+
signalFactory(params) {
|
|
234
|
+
return createSignalFromParams(params);
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
```
|
|
212
238
|
|
|
213
|
-
|
|
239
|
+
Each window picks the best parameter set in training, then runs it blind on the test slice. The `windows` array in the result shows per-window winners. If the winning parameters swing wildly from window to window, that is a real signal — not a formatting detail.
|
|
214
240
|
|
|
215
|
-
|
|
241
|
+
---
|
|
216
242
|
|
|
217
|
-
|
|
243
|
+
## Execution and cost modeling
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
const result = backtest({
|
|
247
|
+
candles,
|
|
248
|
+
signal,
|
|
249
|
+
costs: {
|
|
250
|
+
slippageBps: 2,
|
|
251
|
+
spreadBps: 1,
|
|
252
|
+
slippageByKind: {
|
|
253
|
+
market: 3,
|
|
254
|
+
limit: 0.5,
|
|
255
|
+
stop: 4,
|
|
256
|
+
},
|
|
257
|
+
commissionBps: 1,
|
|
258
|
+
commissionPerUnit: 0,
|
|
259
|
+
commissionPerOrder: 1,
|
|
260
|
+
minCommission: 1,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- Slippage is applied in the trade direction
|
|
266
|
+
- Spread is modeled as half-spread paid on entry and exit
|
|
267
|
+
- Commission can be percentage-based, per-unit, per-order, or mixed
|
|
268
|
+
- `minCommission` floors the fee per fill
|
|
269
|
+
|
|
270
|
+
> Leaving costs at zero is the most common cause of inflated backtests. Set them from the start.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Exports and reporting
|
|
275
|
+
|
|
276
|
+
```js
|
|
277
|
+
import { exportBacktestArtifacts } from "tradelab";
|
|
278
|
+
|
|
279
|
+
// Writes HTML report + trade CSV + metrics JSON in one call
|
|
280
|
+
exportBacktestArtifacts({ result, outDir: "./output" });
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Or use the narrower helpers:
|
|
284
|
+
|
|
285
|
+
| Helper | Output |
|
|
286
|
+
|---|---|
|
|
287
|
+
| `exportHtmlReport(options)` | Interactive HTML report written to disk |
|
|
288
|
+
| `renderHtmlReport(options)` | HTML report returned as a string |
|
|
289
|
+
| `exportTradesCsv(trades, options)` | Flat trade ledger for spreadsheets or pandas |
|
|
290
|
+
| `exportMetricsJSON(options)` | Machine-readable metrics for dashboards or automation |
|
|
291
|
+
|
|
292
|
+
For programmatic pipelines, `exportMetricsJSON` is usually the most useful format to build on.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## CLI
|
|
297
|
+
|
|
298
|
+
The package ships a `tradelab` binary. Best for quick iteration, smoke tests, and trying the package before wiring it into application code.
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Backtest from Yahoo
|
|
302
|
+
npx tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
|
|
303
|
+
|
|
304
|
+
# Backtest from CSV with a built-in strategy
|
|
305
|
+
npx tradelab backtest --source csv --csvPath ./data/btc.csv --strategy buy-hold --holdBars 3
|
|
306
|
+
|
|
307
|
+
# Multi-symbol portfolio
|
|
308
|
+
npx tradelab portfolio \
|
|
309
|
+
--csvPaths ./data/spy.csv,./data/qqq.csv \
|
|
310
|
+
--symbols SPY,QQQ \
|
|
311
|
+
--strategy buy-hold
|
|
312
|
+
|
|
313
|
+
# Walk-forward validation
|
|
314
|
+
npx tradelab walk-forward \
|
|
315
|
+
--source yahoo --symbol QQQ --interval 1d --period 2y \
|
|
316
|
+
--trainBars 180 --testBars 60
|
|
317
|
+
|
|
318
|
+
# Prefetch and cache data
|
|
319
|
+
npx tradelab prefetch --symbol SPY --interval 1d --period 1y
|
|
320
|
+
npx tradelab import-csv --csvPath ./data/spy.csv --symbol SPY --interval 1d
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Built-in strategies:** `ema-cross` · `buy-hold`
|
|
324
|
+
|
|
325
|
+
You can also point `--strategy` at a local module that exports `default(args)`, `createSignal(args)`, or `signal`.
|
|
326
|
+
|
|
327
|
+
---
|
|
218
328
|
|
|
219
329
|
## Examples
|
|
220
330
|
|
|
@@ -223,9 +333,51 @@ node examples/emaCross.js
|
|
|
223
333
|
node examples/yahooEmaCross.js SPY 1d 1y
|
|
224
334
|
```
|
|
225
335
|
|
|
336
|
+
The examples are a good place to start if you want something runnable before wiring the package into your own strategy code.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Importing
|
|
341
|
+
|
|
342
|
+
### ESM
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
import { backtest, getHistoricalCandles, ema } from "tradelab";
|
|
346
|
+
import { fetchHistorical } from "tradelab/data";
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### CommonJS
|
|
350
|
+
|
|
351
|
+
```js
|
|
352
|
+
const { backtest, getHistoricalCandles, ema } = require("tradelab");
|
|
353
|
+
const { fetchHistorical } = require("tradelab/data");
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Documentation
|
|
359
|
+
|
|
360
|
+
| Guide | What it covers |
|
|
361
|
+
|---|---|
|
|
362
|
+
| [Backtest engine](docs/backtest-engine.md) | Signal contract, all options, result shape, portfolio mode, walk-forward |
|
|
363
|
+
| [Data, reporting, and CLI](docs/data-reporting-cli.md) | Data loading, cache behavior, exports, CLI reference |
|
|
364
|
+
| [API reference](docs/api-reference.md) | Compact index of every public export |
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Common mistakes
|
|
369
|
+
|
|
370
|
+
- Using unsorted candles or mixed intervals in a single series
|
|
371
|
+
- Reading `trades` as if they were always full positions — use `positions` for top-line analysis
|
|
372
|
+
- Leaving costs at zero and overestimating edge
|
|
373
|
+
- Trusting one backtest without out-of-sample validation
|
|
374
|
+
- Debugging a strategy with `strict: false` when lookahead is possible
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
226
378
|
## Notes
|
|
227
379
|
|
|
228
|
-
-
|
|
229
|
-
-
|
|
230
|
-
-
|
|
231
|
-
-
|
|
380
|
+
- Node `18+` is required
|
|
381
|
+
- Yahoo downloads are cached under `output/data` by default
|
|
382
|
+
- CommonJS and ESM are both supported
|
|
383
|
+
- The engine is built for historical research — not brokerage execution, tick-level simulation, or exchange microstructure modeling
|