tradelab 0.3.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 +269 -130
- 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 +2 -1
package/README.md
CHANGED
|
@@ -1,55 +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, optional replay/equity capture, and configurable slippage/commission modeling
|
|
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
|
-
- Multi-symbol portfolio aggregation and rolling walk-forward optimization helpers
|
|
22
|
-
- HTML report export, metrics JSON export, and trade CSV export
|
|
23
|
-
- Utility indicators and session helpers for strategy development
|
|
24
|
-
- CLI entrypoint for fetching data and running quick backtests from the terminal
|
|
25
|
-
- TypeScript definitions for the public API
|
|
26
|
-
|
|
27
|
-
## 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.
|
|
28
19
|
|
|
29
20
|
```bash
|
|
30
21
|
npm install tradelab
|
|
31
22
|
```
|
|
32
23
|
|
|
33
|
-
|
|
24
|
+
---
|
|
34
25
|
|
|
35
|
-
##
|
|
26
|
+
## Table of contents
|
|
36
27
|
|
|
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)
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
---
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
import { backtest, getHistoricalCandles, ema } from "tradelab";
|
|
42
|
-
import { fetchHistorical } from "tradelab/data";
|
|
43
|
-
```
|
|
42
|
+
## What it includes
|
|
44
43
|
|
|
45
|
-
|
|
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 |
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
If you already have candles, `backtest()` is the main entry point.
|
|
53
59
|
|
|
54
60
|
```js
|
|
55
61
|
import { backtest, ema, exportBacktestArtifacts } from "tradelab";
|
|
@@ -75,29 +81,23 @@ const result = backtest({
|
|
|
75
81
|
const risk = entry - stop;
|
|
76
82
|
if (risk <= 0) return null;
|
|
77
83
|
|
|
78
|
-
return {
|
|
79
|
-
side: "long",
|
|
80
|
-
entry,
|
|
81
|
-
stop,
|
|
82
|
-
rr: 2,
|
|
83
|
-
};
|
|
84
|
+
return { side: "long", entry, stop, rr: 2 };
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
return null;
|
|
87
88
|
},
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
exportBacktestArtifacts({
|
|
91
|
-
result,
|
|
92
|
-
outDir: "./output",
|
|
93
|
-
});
|
|
91
|
+
exportBacktestArtifacts({ result, outDir: "./output" });
|
|
94
92
|
```
|
|
95
93
|
|
|
96
|
-
|
|
94
|
+
After the run, check `result.metrics` for the headline numbers and `result.positions` for the trade log.
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
---
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
## Loading historical data
|
|
99
|
+
|
|
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()`.
|
|
101
101
|
|
|
102
102
|
```js
|
|
103
103
|
import { getHistoricalCandles, backtest } from "tradelab";
|
|
@@ -107,138 +107,277 @@ const candles = await getHistoricalCandles({
|
|
|
107
107
|
symbol: "SPY",
|
|
108
108
|
interval: "1d",
|
|
109
109
|
period: "2y",
|
|
110
|
-
cache: true,
|
|
110
|
+
cache: true, // reuses local copy on repeated runs
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
const result = backtest({
|
|
114
|
-
candles,
|
|
115
|
-
symbol: "SPY",
|
|
116
|
-
interval: "1d",
|
|
117
|
-
range: "2y",
|
|
118
|
-
signal,
|
|
119
|
-
});
|
|
113
|
+
const result = backtest({ candles, symbol: "SPY", interval: "1d", range: "2y", signal });
|
|
120
114
|
```
|
|
121
115
|
|
|
122
|
-
Supported
|
|
116
|
+
**Supported sources:** `yahoo` · `csv` · `auto`
|
|
123
117
|
|
|
124
|
-
|
|
118
|
+
**Supported periods:** `5d` · `60d` · `6mo` · `1y` · `2y` · and more
|
|
125
119
|
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
Use `cache: true` for repeatable research runs. It eliminates network noise and makes failures easier to diagnose.
|
|
121
|
+
|
|
122
|
+
### CSV import
|
|
128
123
|
|
|
124
|
+
```js
|
|
129
125
|
const candles = await getHistoricalCandles({
|
|
130
126
|
source: "csv",
|
|
131
|
-
|
|
132
|
-
interval: "5m",
|
|
133
|
-
csvPath: "./data/btc-5m.csv",
|
|
127
|
+
csvPath: "./data/spy.csv",
|
|
134
128
|
csv: {
|
|
135
|
-
timeCol: "
|
|
129
|
+
timeCol: "timestamp",
|
|
136
130
|
openCol: "open",
|
|
137
|
-
|
|
138
|
-
lowCol: "low",
|
|
139
|
-
closeCol: "close",
|
|
140
|
-
volumeCol: "volume",
|
|
131
|
+
// ... optional column mapping
|
|
141
132
|
},
|
|
142
133
|
});
|
|
143
134
|
```
|
|
144
135
|
|
|
145
|
-
If
|
|
136
|
+
If your CSV already uses standard OHLCV column names, no mapping is needed at all.
|
|
137
|
+
|
|
138
|
+
---
|
|
146
139
|
|
|
147
|
-
##
|
|
140
|
+
## Core concepts
|
|
148
141
|
|
|
149
|
-
|
|
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.
|
|
150
145
|
|
|
151
146
|
```js
|
|
152
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
};
|
|
159
156
|
}
|
|
160
157
|
```
|
|
161
158
|
|
|
162
|
-
|
|
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
|
|
163
175
|
|
|
164
176
|
```js
|
|
165
177
|
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
170
184
|
}
|
|
171
185
|
```
|
|
172
186
|
|
|
173
|
-
|
|
187
|
+
**First checks after any run:**
|
|
174
188
|
|
|
175
|
-
- `
|
|
176
|
-
- `
|
|
177
|
-
- `
|
|
178
|
-
- `
|
|
179
|
-
- `riskPct` or `riskFraction` can override the global risk setting per signal
|
|
180
|
-
- `strict: true` throws if the strategy directly accesses candles beyond the current index
|
|
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?
|
|
181
193
|
|
|
182
|
-
|
|
194
|
+
---
|
|
183
195
|
|
|
184
|
-
|
|
185
|
-
- `_cooldownBars`
|
|
186
|
-
- `_breakevenAtR`
|
|
187
|
-
- `_trailAfterR`
|
|
188
|
-
- `_maxBarsInTrade`
|
|
189
|
-
- `_maxHoldMin`
|
|
190
|
-
- `_rr`
|
|
191
|
-
- `_initRisk`
|
|
192
|
-
- `_imb`
|
|
196
|
+
## Portfolio mode
|
|
193
197
|
|
|
194
|
-
|
|
198
|
+
Use `backtestPortfolio()` when you have one candle array per symbol and want a single combined result.
|
|
195
199
|
|
|
196
|
-
|
|
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
|
+
```
|
|
197
211
|
|
|
198
|
-
-
|
|
199
|
-
- `positions`: completed positions only
|
|
200
|
-
- `metrics`: aggregate stats including `winRate`, `expectancy`, `profitFactor`, `maxDrawdown`, `sharpe`, `avgHold`, and `sideBreakdown`
|
|
201
|
-
- `eqSeries`: realized equity history as `{ time, timestamp, equity }`
|
|
202
|
-
- `replay`: chart-friendly frame and event data
|
|
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.
|
|
203
213
|
|
|
204
|
-
|
|
214
|
+
---
|
|
205
215
|
|
|
206
|
-
-
|
|
207
|
-
- `backtestPortfolio({ systems, equity })`
|
|
208
|
-
- `walkForwardOptimize({ candles, signalFactory, parameterSets, trainBars, testBars })`
|
|
209
|
-
- `backtestHistorical({ data, backtestOptions })`
|
|
210
|
-
- `getHistoricalCandles(options)`
|
|
211
|
-
- `fetchHistorical(symbol, interval, period)`
|
|
212
|
-
- `loadCandlesFromCSV(filePath, options)`
|
|
213
|
-
- `saveCandlesToCache(candles, meta)`
|
|
214
|
-
- `loadCandlesFromCache(symbol, interval, period, outDir)`
|
|
215
|
-
- `exportMetricsJSON({ result, outDir })`
|
|
216
|
-
- `exportBacktestArtifacts({ result, outDir })`
|
|
216
|
+
## Walk-forward optimization
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
Use `walkForwardOptimize()` when one in-sample backtest is not enough. It runs rolling train/test windows across the full candle history.
|
|
219
219
|
|
|
220
|
-
|
|
220
|
+
```js
|
|
221
|
+
import { walkForwardOptimize } from "tradelab";
|
|
221
222
|
|
|
222
|
-
|
|
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
|
+
```
|
|
223
238
|
|
|
224
|
-
|
|
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.
|
|
225
240
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
241
|
+
---
|
|
242
|
+
|
|
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
|
+
});
|
|
229
263
|
```
|
|
230
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
|
+
|
|
231
296
|
## CLI
|
|
232
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
|
+
|
|
233
300
|
```bash
|
|
301
|
+
# Backtest from Yahoo
|
|
234
302
|
npx tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
|
|
303
|
+
|
|
304
|
+
# Backtest from CSV with a built-in strategy
|
|
235
305
|
npx tradelab backtest --source csv --csvPath ./data/btc.csv --strategy buy-hold --holdBars 3
|
|
236
|
-
|
|
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
|
|
237
321
|
```
|
|
238
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
|
+
---
|
|
328
|
+
|
|
329
|
+
## Examples
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
node examples/emaCross.js
|
|
333
|
+
node examples/yahooEmaCross.js SPY 1d 1y
|
|
334
|
+
```
|
|
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
|
+
|
|
239
378
|
## Notes
|
|
240
379
|
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
244
|
-
-
|
|
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
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# tradelab docs
|
|
2
|
+
|
|
3
|
+
## Guides
|
|
4
|
+
|
|
5
|
+
- [Backtest engine](backtest-engine.md)
|
|
6
|
+
- [Data, reporting, and CLI](data-reporting-cli.md)
|
|
7
|
+
- [API reference](api-reference.md)
|
|
8
|
+
|
|
9
|
+
## Choose a path
|
|
10
|
+
|
|
11
|
+
| Goal | Start here |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| Run one strategy on one dataset | [Backtest engine](backtest-engine.md) |
|
|
14
|
+
| Load Yahoo or CSV data | [Data, reporting, and CLI](data-reporting-cli.md) |
|
|
15
|
+
| Export reports or machine-readable results | [Data, reporting, and CLI](data-reporting-cli.md) |
|
|
16
|
+
| Run multiple symbols together | [Backtest engine](backtest-engine.md) |
|
|
17
|
+
| Run walk-forward validation | [Backtest engine](backtest-engine.md) |
|
|
18
|
+
| Check the exact public exports | [API reference](api-reference.md) |
|
|
19
|
+
|
|
20
|
+
## Package scope
|
|
21
|
+
|
|
22
|
+
tradelab is built for:
|
|
23
|
+
|
|
24
|
+
- candle-based strategy research
|
|
25
|
+
- historical backtests with configurable fills and costs
|
|
26
|
+
- CSV and Yahoo-based data workflows
|
|
27
|
+
- exportable outputs for review or automation
|
|
28
|
+
|
|
29
|
+
tradelab is not built for:
|
|
30
|
+
|
|
31
|
+
- live broker execution
|
|
32
|
+
- tick-level simulation
|
|
33
|
+
- exchange microstructure modeling
|
|
34
|
+
|
|
35
|
+
## Common workflows
|
|
36
|
+
|
|
37
|
+
### Single strategy workflow
|
|
38
|
+
|
|
39
|
+
1. Load candles with `getHistoricalCandles()` or your own dataset
|
|
40
|
+
2. Run `backtest()`
|
|
41
|
+
3. Inspect `result.metrics` and `result.positions`
|
|
42
|
+
4. Export HTML, CSV, or JSON if needed
|
|
43
|
+
|
|
44
|
+
### Multi-symbol workflow
|
|
45
|
+
|
|
46
|
+
1. Prepare one candle array per symbol
|
|
47
|
+
2. Run `backtestPortfolio()`
|
|
48
|
+
3. Review combined `metrics`, `positions`, and `eqSeries`
|
|
49
|
+
|
|
50
|
+
### Validation workflow
|
|
51
|
+
|
|
52
|
+
1. Build a `signalFactory(params)`
|
|
53
|
+
2. Create parameter sets
|
|
54
|
+
3. Run `walkForwardOptimize()`
|
|
55
|
+
4. Review per-window winners before trusting the aggregate result
|
|
56
|
+
|
|
57
|
+
## Documentation map
|
|
58
|
+
|
|
59
|
+
- [Backtest engine](backtest-engine.md): strategy inputs, engine options, result shape, portfolio mode, walk-forward mode
|
|
60
|
+
- [Data, reporting, and CLI](data-reporting-cli.md): data loading, cache behavior, exports, terminal usage
|
|
61
|
+
- [API reference](api-reference.md): compact export index
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
This page is the compact index of public exports.
|
|
4
|
+
|
|
5
|
+
If you are learning the package, start with [backtest-engine.md](backtest-engine.md) or [data-reporting-cli.md](data-reporting-cli.md). This page is for quick lookup.
|
|
6
|
+
|
|
7
|
+
## Backtesting
|
|
8
|
+
|
|
9
|
+
| Export | Summary |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `backtest(options)` | Run one strategy on one candle series |
|
|
12
|
+
| `backtestPortfolio(options)` | Run multiple systems and merge the result |
|
|
13
|
+
| `walkForwardOptimize(options)` | Run rolling train/test validation |
|
|
14
|
+
| `buildMetrics(input)` | Compute metrics from realized trades and equity data |
|
|
15
|
+
|
|
16
|
+
## Data
|
|
17
|
+
|
|
18
|
+
| Export | Summary |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `getHistoricalCandles(options)` | Load candles from Yahoo or CSV |
|
|
21
|
+
| `backtestHistorical({ data, backtestOptions })` | Load candles and immediately run `backtest()` |
|
|
22
|
+
| `fetchHistorical(symbol, interval, period, options)` | Call the Yahoo layer directly |
|
|
23
|
+
| `fetchLatestCandle(symbol, interval, options)` | Fetch the latest Yahoo candle |
|
|
24
|
+
| `loadCandlesFromCSV(filePath, options)` | Parse and normalize a CSV file |
|
|
25
|
+
| `normalizeCandles(candles)` | Normalize candle field names and sort/dedupe |
|
|
26
|
+
| `mergeCandles(...arrays)` | Merge multiple candle arrays |
|
|
27
|
+
| `candleStats(candles)` | Return summary stats for a candle array |
|
|
28
|
+
| `saveCandlesToCache(candles, meta)` | Write normalized candles to the local cache |
|
|
29
|
+
| `loadCandlesFromCache(symbol, interval, period, outDir)` | Read normalized candles from the local cache |
|
|
30
|
+
| `cachedCandlesPath(symbol, interval, period, outDir)` | Return the expected cache path |
|
|
31
|
+
|
|
32
|
+
## Reporting
|
|
33
|
+
|
|
34
|
+
| Export | Summary |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `renderHtmlReport(options)` | Return the HTML report as a string |
|
|
37
|
+
| `exportHtmlReport(options)` | Write the HTML report to disk |
|
|
38
|
+
| `exportTradesCsv(trades, options)` | Write a CSV ledger of trades or positions |
|
|
39
|
+
| `exportMetricsJSON(options)` | Write machine-readable metrics JSON |
|
|
40
|
+
| `exportBacktestArtifacts(options)` | Write HTML, CSV, and metrics JSON together |
|
|
41
|
+
|
|
42
|
+
## Indicators and utilities
|
|
43
|
+
|
|
44
|
+
### Indicators
|
|
45
|
+
|
|
46
|
+
- `ema(values, period)`
|
|
47
|
+
- `atr(bars, period)`
|
|
48
|
+
- `swingHigh(bars, index, left, right)`
|
|
49
|
+
- `swingLow(bars, index, left, right)`
|
|
50
|
+
- `detectFVG(bars, index)`
|
|
51
|
+
- `lastSwing(bars, index, direction)`
|
|
52
|
+
- `structureState(bars, index)`
|
|
53
|
+
- `bpsOf(price, bps)`
|
|
54
|
+
- `pct(a, b)`
|
|
55
|
+
|
|
56
|
+
### Position sizing
|
|
57
|
+
|
|
58
|
+
- `calculatePositionSize(input)`
|
|
59
|
+
|
|
60
|
+
### Time helpers
|
|
61
|
+
|
|
62
|
+
- `offsetET(timeMs)`
|
|
63
|
+
- `minutesET(timeMs)`
|
|
64
|
+
- `isSession(timeMs, session)`
|
|
65
|
+
- `parseWindowsCSV(csv)`
|
|
66
|
+
- `inWindowsET(timeMs, windows)`
|
|
67
|
+
|
|
68
|
+
## Types
|
|
69
|
+
|
|
70
|
+
The package ships declarations in [../types/index.d.ts](../types/index.d.ts). Use that file when you need the exact option and result contracts in TypeScript or editor IntelliSense.
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# Backtest engine
|
|
2
|
+
|
|
3
|
+
This page covers the simulation layer:
|
|
4
|
+
|
|
5
|
+
- `backtest(options)`
|
|
6
|
+
- `backtestPortfolio(options)`
|
|
7
|
+
- `walkForwardOptimize(options)`
|
|
8
|
+
- `buildMetrics(input)`
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Use the engine layer when you already have candles and want to simulate strategy behavior, inspect the result, and export or post-process it.
|
|
13
|
+
|
|
14
|
+
## Choose the right function
|
|
15
|
+
|
|
16
|
+
| Use case | Function |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| One strategy on one candle series | `backtest()` |
|
|
19
|
+
| Multiple symbols with one combined result | `backtestPortfolio()` |
|
|
20
|
+
| Rolling train/test validation | `walkForwardOptimize()` |
|
|
21
|
+
| Recompute metrics from realized trades | `buildMetrics()` |
|
|
22
|
+
|
|
23
|
+
## Candle input
|
|
24
|
+
|
|
25
|
+
Candles should be sorted in ascending time order.
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
{
|
|
29
|
+
time: 1735828200000,
|
|
30
|
+
open: 100,
|
|
31
|
+
high: 102,
|
|
32
|
+
low: 99,
|
|
33
|
+
close: 101,
|
|
34
|
+
volume: 1000
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The package also normalizes common aliases such as `timestamp`, `date`, `o`, `h`, `l`, and `c`.
|
|
39
|
+
|
|
40
|
+
## `backtest(options)`
|
|
41
|
+
|
|
42
|
+
`backtest()` is the main single-symbol entry point.
|
|
43
|
+
|
|
44
|
+
### Minimal example
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { backtest } from "tradelab";
|
|
48
|
+
|
|
49
|
+
const result = backtest({
|
|
50
|
+
candles,
|
|
51
|
+
signal({ bar, index }) {
|
|
52
|
+
if (index !== 20) return null;
|
|
53
|
+
return {
|
|
54
|
+
side: "long",
|
|
55
|
+
entry: bar.close,
|
|
56
|
+
stop: bar.close - 2,
|
|
57
|
+
rr: 2,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Required fields
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
{
|
|
67
|
+
candles: Candle[],
|
|
68
|
+
signal: ({ candles, index, bar, equity, openPosition, pendingOrder }) => Signal | null
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Core options
|
|
73
|
+
|
|
74
|
+
| Option | Purpose |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `symbol`, `interval`, `range` | Labels carried into results and exports |
|
|
77
|
+
| `equity` | Starting equity, default `10000` |
|
|
78
|
+
| `riskPct` or `riskFraction` | Default risk per trade when `qty` is not provided |
|
|
79
|
+
| `warmupBars` | Bars skipped before signal evaluation starts |
|
|
80
|
+
| `flattenAtClose` | Forces end-of-day exit when enabled |
|
|
81
|
+
| `collectEqSeries`, `collectReplay` | Builds extra output for charts and exports |
|
|
82
|
+
| `strict` | Throws on direct lookahead access such as `candles[index + 1]` |
|
|
83
|
+
| `costs` | Slippage, spread, and commission model |
|
|
84
|
+
|
|
85
|
+
If you are starting from scratch, the most useful options to set explicitly are:
|
|
86
|
+
|
|
87
|
+
- `equity`
|
|
88
|
+
- `riskPct`
|
|
89
|
+
- `warmupBars`
|
|
90
|
+
- `flattenAtClose`
|
|
91
|
+
- `costs`
|
|
92
|
+
|
|
93
|
+
### Signal contract
|
|
94
|
+
|
|
95
|
+
The signal function receives:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
{
|
|
99
|
+
candles,
|
|
100
|
+
index,
|
|
101
|
+
bar,
|
|
102
|
+
equity,
|
|
103
|
+
openPosition,
|
|
104
|
+
pendingOrder
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Return `null` for no trade, or a signal object:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
{
|
|
112
|
+
side: "long" | "short",
|
|
113
|
+
entry: 101.25,
|
|
114
|
+
stop: 99.75,
|
|
115
|
+
takeProfit: 104.25
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Signal conveniences
|
|
120
|
+
|
|
121
|
+
| Field | Behavior |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| `side` | Accepts `long`, `short`, `buy`, or `sell` |
|
|
124
|
+
| `entry` | Defaults to the current close if omitted |
|
|
125
|
+
| `takeProfit` | Can be derived from `rr` or `_rr` |
|
|
126
|
+
| `qty` or `size` | Overrides risk-based sizing |
|
|
127
|
+
| `riskPct` or `riskFraction` | Overrides the global risk setting for that trade |
|
|
128
|
+
|
|
129
|
+
Practical rule: return the smallest signal object that expresses the trade clearly. In many strategies that is just `side`, `stop`, and `rr`.
|
|
130
|
+
|
|
131
|
+
### Optional per-trade hints
|
|
132
|
+
|
|
133
|
+
These values are read from the signal object when present:
|
|
134
|
+
|
|
135
|
+
- `_entryExpiryBars`
|
|
136
|
+
- `_cooldownBars`
|
|
137
|
+
- `_breakevenAtR`
|
|
138
|
+
- `_trailAfterR`
|
|
139
|
+
- `_maxBarsInTrade`
|
|
140
|
+
- `_maxHoldMin`
|
|
141
|
+
- `_rr`
|
|
142
|
+
- `_initRisk`
|
|
143
|
+
- `_imb`
|
|
144
|
+
|
|
145
|
+
### Execution and cost model
|
|
146
|
+
|
|
147
|
+
Legacy options still work:
|
|
148
|
+
|
|
149
|
+
- `slippageBps`
|
|
150
|
+
- `feeBps`
|
|
151
|
+
|
|
152
|
+
For more control, use `costs`:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
{
|
|
156
|
+
costs: {
|
|
157
|
+
slippageBps: 2,
|
|
158
|
+
spreadBps: 1,
|
|
159
|
+
slippageByKind: {
|
|
160
|
+
market: 3,
|
|
161
|
+
limit: 0.5,
|
|
162
|
+
stop: 4,
|
|
163
|
+
},
|
|
164
|
+
commissionBps: 1,
|
|
165
|
+
commissionPerUnit: 0,
|
|
166
|
+
commissionPerOrder: 1,
|
|
167
|
+
minCommission: 1,
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Cost model behavior
|
|
173
|
+
|
|
174
|
+
- slippage is applied in trade direction
|
|
175
|
+
- spread is modeled as half-spread paid on entry and exit
|
|
176
|
+
- commission can be percentage-based, per-unit, per-order, or mixed
|
|
177
|
+
- `minCommission` floors the fee for that fill
|
|
178
|
+
|
|
179
|
+
This is still a bar-based simulation. It does not model queue position, exchange microstructure, or realistic intrabar order priority.
|
|
180
|
+
|
|
181
|
+
### Advanced trade management
|
|
182
|
+
|
|
183
|
+
These are optional. Ignore them until the strategy actually needs them.
|
|
184
|
+
|
|
185
|
+
- `scaleOutAtR`, `scaleOutFrac`, `finalTP_R`
|
|
186
|
+
- `maxDailyLossPct`, `dailyMaxTrades`, `postLossCooldownBars`
|
|
187
|
+
- `atrTrailMult`, `atrTrailPeriod`
|
|
188
|
+
- `mfeTrail`
|
|
189
|
+
- `pyramiding`
|
|
190
|
+
- `volScale`
|
|
191
|
+
- `entryChase`
|
|
192
|
+
- `qtyStep`, `minQty`, `maxLeverage`
|
|
193
|
+
- `reanchorStopOnFill`, `maxSlipROnFill`
|
|
194
|
+
- `oco`
|
|
195
|
+
- `triggerMode`
|
|
196
|
+
|
|
197
|
+
Recommended order of adoption:
|
|
198
|
+
|
|
199
|
+
1. Start with `entry`, `stop`, and `rr`
|
|
200
|
+
2. Add `costs`
|
|
201
|
+
3. Add trailing, scale-outs, or pyramiding only if the real strategy uses them
|
|
202
|
+
|
|
203
|
+
## Result shape
|
|
204
|
+
|
|
205
|
+
`backtest()` returns:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
{
|
|
209
|
+
symbol,
|
|
210
|
+
interval,
|
|
211
|
+
range,
|
|
212
|
+
trades,
|
|
213
|
+
positions,
|
|
214
|
+
metrics,
|
|
215
|
+
eqSeries,
|
|
216
|
+
replay
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### `trades`
|
|
221
|
+
|
|
222
|
+
Every realized leg, including partial exits and scale-outs.
|
|
223
|
+
|
|
224
|
+
### `positions`
|
|
225
|
+
|
|
226
|
+
Completed positions only. This is the collection most users want for top-line analysis.
|
|
227
|
+
|
|
228
|
+
If you are unsure whether to use `trades` or `positions`, start with `positions`.
|
|
229
|
+
|
|
230
|
+
### `metrics`
|
|
231
|
+
|
|
232
|
+
Most users start with:
|
|
233
|
+
|
|
234
|
+
- `trades`
|
|
235
|
+
- `winRate`
|
|
236
|
+
- `expectancy`
|
|
237
|
+
- `profitFactor`
|
|
238
|
+
- `maxDrawdown`
|
|
239
|
+
- `sharpe`
|
|
240
|
+
- `avgHold`
|
|
241
|
+
- `returnPct`
|
|
242
|
+
- `totalPnL`
|
|
243
|
+
- `finalEquity`
|
|
244
|
+
- `sideBreakdown`
|
|
245
|
+
|
|
246
|
+
Also included:
|
|
247
|
+
|
|
248
|
+
- position-vs-leg variants such as `profitFactor_pos` and `profitFactor_leg`
|
|
249
|
+
- `rDist` percentiles
|
|
250
|
+
- `holdDistMin` percentiles
|
|
251
|
+
- daily stats under `daily`
|
|
252
|
+
|
|
253
|
+
Useful first checks after any run:
|
|
254
|
+
|
|
255
|
+
- `metrics.trades`: enough sample size to care
|
|
256
|
+
- `metrics.profitFactor`: whether winners beat losers gross of the chosen fill model
|
|
257
|
+
- `metrics.maxDrawdown`: whether the path is survivable
|
|
258
|
+
- `metrics.sideBreakdown`: whether one side carries the result
|
|
259
|
+
|
|
260
|
+
### `eqSeries`
|
|
261
|
+
|
|
262
|
+
Realized equity points:
|
|
263
|
+
|
|
264
|
+
```js
|
|
265
|
+
[
|
|
266
|
+
{ time, timestamp, equity }
|
|
267
|
+
]
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`time` and `timestamp` contain the same Unix-millisecond value.
|
|
271
|
+
|
|
272
|
+
### `replay`
|
|
273
|
+
|
|
274
|
+
Visualization payload:
|
|
275
|
+
|
|
276
|
+
```js
|
|
277
|
+
{
|
|
278
|
+
frames: [{ t, price, equity, posSide, posSize }],
|
|
279
|
+
events: [{ t, price, type, side, size, tradeId, reason, pnl }]
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
This is meant for charts and reports, not as a full audit log.
|
|
284
|
+
|
|
285
|
+
## `backtestPortfolio(options)`
|
|
286
|
+
|
|
287
|
+
Use portfolio mode when you already have one candle array per symbol and want one combined result.
|
|
288
|
+
|
|
289
|
+
```js
|
|
290
|
+
const result = backtestPortfolio({
|
|
291
|
+
equity: 100_000,
|
|
292
|
+
systems: [
|
|
293
|
+
{ symbol: "SPY", candles: spy, signal: signalA, weight: 2 },
|
|
294
|
+
{ symbol: "QQQ", candles: qqq, signal: signalB, weight: 1 },
|
|
295
|
+
],
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### How it works
|
|
300
|
+
|
|
301
|
+
- capital is allocated up front by weight
|
|
302
|
+
- each system runs through the normal single-symbol engine
|
|
303
|
+
- the portfolio result merges trades, positions, replay events, and equity series
|
|
304
|
+
|
|
305
|
+
### What it is not
|
|
306
|
+
|
|
307
|
+
- a cross-margin broker simulator
|
|
308
|
+
- a portfolio-level fill arbiter
|
|
309
|
+
- a shared capital-locking engine
|
|
310
|
+
|
|
311
|
+
If you need shared real-time portfolio constraints, this is not that tool yet.
|
|
312
|
+
|
|
313
|
+
## `walkForwardOptimize(options)`
|
|
314
|
+
|
|
315
|
+
Use walk-forward mode when one in-sample backtest is not enough and you want rolling train/test validation.
|
|
316
|
+
|
|
317
|
+
```js
|
|
318
|
+
const wf = walkForwardOptimize({
|
|
319
|
+
candles,
|
|
320
|
+
trainBars: 180,
|
|
321
|
+
testBars: 60,
|
|
322
|
+
stepBars: 60,
|
|
323
|
+
scoreBy: "profitFactor",
|
|
324
|
+
parameterSets: [
|
|
325
|
+
{ fast: 8, slow: 21, rr: 2 },
|
|
326
|
+
{ fast: 10, slow: 30, rr: 2 },
|
|
327
|
+
],
|
|
328
|
+
signalFactory(params) {
|
|
329
|
+
return createSignalFromParams(params);
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### How it works
|
|
335
|
+
|
|
336
|
+
1. Evaluate every parameter set on the training slice
|
|
337
|
+
2. Pick the best one by `scoreBy`
|
|
338
|
+
3. Run that parameter set on the next test slice
|
|
339
|
+
4. Repeat for each window
|
|
340
|
+
|
|
341
|
+
### Return value
|
|
342
|
+
|
|
343
|
+
- `windows`: per-window summaries and chosen parameters
|
|
344
|
+
- `trades`, `positions`, `metrics`, `eqSeries`
|
|
345
|
+
- `bestParams`: chosen parameters for each window
|
|
346
|
+
|
|
347
|
+
In practice, the per-window output matters more than the aggregate headline. If the winning parameters swing wildly from one window to the next, treat that as a real signal.
|
|
348
|
+
|
|
349
|
+
## `buildMetrics(input)`
|
|
350
|
+
|
|
351
|
+
Most users do not need this directly. Use it when:
|
|
352
|
+
|
|
353
|
+
- you generate realized trades outside `backtest()`
|
|
354
|
+
- you filter a result and want fresh metrics
|
|
355
|
+
- you combine results manually
|
|
356
|
+
|
|
357
|
+
## Common mistakes
|
|
358
|
+
|
|
359
|
+
- using unsorted candles or mixed intervals in one series
|
|
360
|
+
- reading `trades` as if they were always full positions
|
|
361
|
+
- leaving costs at zero and overestimating edge
|
|
362
|
+
- trusting one backtest without out-of-sample validation
|
|
363
|
+
- debugging a strategy with `strict: false` when lookahead is possible
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Data, reporting, and CLI
|
|
2
|
+
|
|
3
|
+
This page covers the parts of the package around the core engine:
|
|
4
|
+
|
|
5
|
+
- historical data loading
|
|
6
|
+
- local cache helpers
|
|
7
|
+
- export helpers
|
|
8
|
+
- command-line usage
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
If you are not bringing your own candles yet, start here.
|
|
13
|
+
|
|
14
|
+
## Choose the right entry point
|
|
15
|
+
|
|
16
|
+
| Use case | Function |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Load data without caring about the source-specific helper | `getHistoricalCandles()` |
|
|
19
|
+
| Fetch directly from Yahoo | `fetchHistorical()` |
|
|
20
|
+
| Load a local CSV file | `loadCandlesFromCSV()` |
|
|
21
|
+
| Reuse saved normalized data | `loadCandlesFromCache()` |
|
|
22
|
+
| Try the package from a terminal first | `tradelab` CLI |
|
|
23
|
+
|
|
24
|
+
## Historical data
|
|
25
|
+
|
|
26
|
+
### `getHistoricalCandles(options)`
|
|
27
|
+
|
|
28
|
+
This is the main data-loading entry point.
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const candles = await getHistoricalCandles({
|
|
32
|
+
source: "yahoo",
|
|
33
|
+
symbol: "SPY",
|
|
34
|
+
interval: "1d",
|
|
35
|
+
period: "2y",
|
|
36
|
+
cache: true,
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Sources
|
|
41
|
+
|
|
42
|
+
- `yahoo`
|
|
43
|
+
- `csv`
|
|
44
|
+
- `auto`
|
|
45
|
+
|
|
46
|
+
`auto` switches to CSV when `csvPath` or `csv.filePath` is present. Otherwise it uses Yahoo.
|
|
47
|
+
|
|
48
|
+
If you are writing application code, prefer `getHistoricalCandles()` over calling source-specific helpers directly.
|
|
49
|
+
|
|
50
|
+
### Yahoo options
|
|
51
|
+
|
|
52
|
+
| Option | Purpose |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `symbol` | Ticker or Yahoo symbol |
|
|
55
|
+
| `interval` | Candle interval such as `1d` or `5m` |
|
|
56
|
+
| `period` | Lookback period such as `6mo` or `1y` |
|
|
57
|
+
| `includePrePost` | Includes premarket and postmarket data when supported |
|
|
58
|
+
| `cache` | Reuses saved normalized data |
|
|
59
|
+
| `refresh` | Forces a fresh download even if cache exists |
|
|
60
|
+
| `cacheDir` | Overrides the default cache directory |
|
|
61
|
+
|
|
62
|
+
The Yahoo layer retries transient failures with exponential backoff. If the endpoint still fails, the error message points users toward CSV or cached data.
|
|
63
|
+
|
|
64
|
+
Use caching for repeatable research runs. It reduces network noise and makes failures easier to diagnose.
|
|
65
|
+
|
|
66
|
+
### CSV options
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
const candles = await getHistoricalCandles({
|
|
70
|
+
source: "csv",
|
|
71
|
+
csvPath: "./data/spy.csv",
|
|
72
|
+
csv: {
|
|
73
|
+
timeCol: "timestamp",
|
|
74
|
+
openCol: "open",
|
|
75
|
+
highCol: "high",
|
|
76
|
+
lowCol: "low",
|
|
77
|
+
closeCol: "close",
|
|
78
|
+
volumeCol: "volume",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
CSV parsing can be configured with:
|
|
84
|
+
|
|
85
|
+
- delimiter
|
|
86
|
+
- header presence
|
|
87
|
+
- column names or indexes
|
|
88
|
+
- start/end date filters
|
|
89
|
+
- custom date parsing
|
|
90
|
+
|
|
91
|
+
If your CSV already uses common OHLCV column names, you often do not need to pass any mapping at all.
|
|
92
|
+
|
|
93
|
+
## Cache helpers
|
|
94
|
+
|
|
95
|
+
Available helpers:
|
|
96
|
+
|
|
97
|
+
- `saveCandlesToCache(candles, meta)`
|
|
98
|
+
- `loadCandlesFromCache(symbol, interval, period, outDir)`
|
|
99
|
+
- `cachedCandlesPath(symbol, interval, period, outDir)`
|
|
100
|
+
|
|
101
|
+
The cache is just normalized candle JSON on disk. It is meant for research convenience, not as a durable database layer.
|
|
102
|
+
|
|
103
|
+
## Common workflows
|
|
104
|
+
|
|
105
|
+
### Yahoo to backtest
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const candles = await getHistoricalCandles({
|
|
109
|
+
source: "yahoo",
|
|
110
|
+
symbol: "SPY",
|
|
111
|
+
interval: "1d",
|
|
112
|
+
period: "1y",
|
|
113
|
+
cache: true,
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### CSV to backtest
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
const candles = await getHistoricalCandles({
|
|
121
|
+
source: "csv",
|
|
122
|
+
csvPath: "./data/spy.csv",
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Cached repeat run
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const candles = await getHistoricalCandles({
|
|
130
|
+
source: "yahoo",
|
|
131
|
+
symbol: "SPY",
|
|
132
|
+
interval: "1d",
|
|
133
|
+
period: "1y",
|
|
134
|
+
cache: true,
|
|
135
|
+
refresh: false,
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Reporting and exports
|
|
140
|
+
|
|
141
|
+
### `exportBacktestArtifacts({ result, outDir })`
|
|
142
|
+
|
|
143
|
+
The main bundle export. By default it writes:
|
|
144
|
+
|
|
145
|
+
- HTML report
|
|
146
|
+
- trade CSV
|
|
147
|
+
- metrics JSON
|
|
148
|
+
|
|
149
|
+
Return value:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
{
|
|
153
|
+
csv,
|
|
154
|
+
html,
|
|
155
|
+
metrics
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
If you only need one output type, call the narrower helper directly.
|
|
160
|
+
|
|
161
|
+
### `exportMetricsJSON({ result, outDir })`
|
|
162
|
+
|
|
163
|
+
Use this for dashboards, notebooks, or any machine-readable downstream pipeline.
|
|
164
|
+
|
|
165
|
+
For automation, this is usually the best export format to build on.
|
|
166
|
+
|
|
167
|
+
### `exportTradesCsv(trades, options)`
|
|
168
|
+
|
|
169
|
+
Use this when you want a flat trade ledger for spreadsheets or pandas-style workflows.
|
|
170
|
+
|
|
171
|
+
### `renderHtmlReport(options)` and `exportHtmlReport(options)`
|
|
172
|
+
|
|
173
|
+
- `renderHtmlReport()` returns an HTML string
|
|
174
|
+
- `exportHtmlReport()` writes the file and returns its path
|
|
175
|
+
|
|
176
|
+
The report system uses the assets under `templates/`. The renderer injects the payload and keeps markup, CSS, and client script separate from the JS entrypoint.
|
|
177
|
+
|
|
178
|
+
## CLI
|
|
179
|
+
|
|
180
|
+
The package ships with a `tradelab` binary.
|
|
181
|
+
|
|
182
|
+
The CLI is best for quick iteration, smoke tests, and trying the package before building a JS workflow around it.
|
|
183
|
+
|
|
184
|
+
## Commands
|
|
185
|
+
|
|
186
|
+
| Command | Purpose |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `tradelab backtest` | Run a single backtest from Yahoo or CSV |
|
|
189
|
+
| `tradelab portfolio` | Run a simple multi-file portfolio backtest |
|
|
190
|
+
| `tradelab walk-forward` | Run rolling validation with the built-in search |
|
|
191
|
+
| `tradelab prefetch` | Download and cache Yahoo data |
|
|
192
|
+
| `tradelab import-csv` | Normalize and cache a CSV file |
|
|
193
|
+
|
|
194
|
+
### Backtest
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
|
|
198
|
+
tradelab backtest --source csv --csvPath ./data/btc.csv --strategy buy-hold --holdBars 3
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Built-in strategies:
|
|
202
|
+
|
|
203
|
+
- `ema-cross`
|
|
204
|
+
- `buy-hold`
|
|
205
|
+
|
|
206
|
+
You can also point `--strategy` at a local module. The module should export one of:
|
|
207
|
+
|
|
208
|
+
- `default(args)`
|
|
209
|
+
- `createSignal(args)`
|
|
210
|
+
- `signal`
|
|
211
|
+
|
|
212
|
+
That makes it easy to prototype a strategy file before wiring it into a larger application.
|
|
213
|
+
|
|
214
|
+
### Portfolio
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
tradelab portfolio \
|
|
218
|
+
--csvPaths ./data/spy.csv,./data/qqq.csv \
|
|
219
|
+
--symbols SPY,QQQ \
|
|
220
|
+
--strategy buy-hold
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This command is intentionally simple. Use it for quick combined runs, not for custom portfolio logic.
|
|
224
|
+
|
|
225
|
+
### Walk-forward
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
tradelab walk-forward \
|
|
229
|
+
--source yahoo \
|
|
230
|
+
--symbol QQQ \
|
|
231
|
+
--interval 1d \
|
|
232
|
+
--period 2y \
|
|
233
|
+
--trainBars 180 \
|
|
234
|
+
--testBars 60
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The CLI walk-forward command currently uses the built-in `ema-cross` parameter search.
|
|
238
|
+
|
|
239
|
+
### Cache utilities
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
tradelab prefetch --symbol SPY --interval 1d --period 1y
|
|
243
|
+
tradelab import-csv --csvPath ./data/spy.csv --symbol SPY --interval 1d
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Troubleshooting
|
|
247
|
+
|
|
248
|
+
| Problem | Check first |
|
|
249
|
+
| --- | --- |
|
|
250
|
+
| Yahoo request errors | enable cache, retry later, or fall back to CSV |
|
|
251
|
+
| Unexpected trade count | `warmupBars`, `flattenAtClose`, and signal frequency |
|
|
252
|
+
| Empty result | candle order, signal logic, and stop/target validity |
|
|
253
|
+
| Confusing CSV import | inspect normalized bars from `loadCandlesFromCSV()` before backtesting |
|
|
254
|
+
| Export confusion | use metrics JSON first if you need programmatic output |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tradelab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Backtesting toolkit for Node.js with strategy simulation, historical data loading, and report generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.cjs",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"bin",
|
|
40
|
+
"docs",
|
|
40
41
|
"dist",
|
|
41
42
|
"src",
|
|
42
43
|
"types",
|