tradelab 0.3.0 → 0.5.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 +290 -130
- package/bin/tradelab.js +68 -23
- package/dist/cjs/data.cjs +78 -53
- package/dist/cjs/index.cjs +1518 -211
- package/docs/README.md +66 -0
- package/docs/api-reference.md +75 -0
- package/docs/backtest-engine.md +393 -0
- package/docs/data-reporting-cli.md +258 -0
- package/docs/examples.md +281 -0
- package/package.json +2 -1
- package/src/engine/backtestTicks.js +429 -0
- package/src/engine/barSystemRunner.js +963 -0
- package/src/engine/portfolio.js +191 -68
- package/src/engine/walkForward.js +106 -10
- package/src/index.js +1 -0
- package/src/metrics/buildMetrics.js +89 -63
- package/types/index.d.ts +77 -1
package/README.md
CHANGED
|
@@ -1,55 +1,62 @@
|
|
|
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 data 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
|
+
- [Tick backtests](#tick-backtests)
|
|
35
|
+
- [Execution and cost modeling](#execution-and-cost-modeling)
|
|
36
|
+
- [Exports and reporting](#exports-and-reporting)
|
|
37
|
+
- [CLI](#cli)
|
|
38
|
+
- [Examples](#examples)
|
|
39
|
+
- [Documentation](#documentation)
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
---
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
import { backtest, getHistoricalCandles, ema } from "tradelab";
|
|
42
|
-
import { fetchHistorical } from "tradelab/data";
|
|
43
|
-
```
|
|
43
|
+
## What it includes
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
| Area | What you get |
|
|
46
|
+
|---|---|
|
|
47
|
+
| **Engine** | Candle and tick backtests with position sizing, exits, replay capture, and cost models |
|
|
48
|
+
| **Portfolio** | Multi-system shared-capital simulation with live capital locking and daily loss halts |
|
|
49
|
+
| **Walk-forward** | Rolling and anchored train/test validation with parameter search and stability summaries |
|
|
50
|
+
| **Data** | Yahoo Finance downloads, CSV import, and local cache helpers |
|
|
51
|
+
| **Costs** | Slippage, spread, and commission modeling |
|
|
52
|
+
| **Exports** | HTML reports, metrics JSON, and trade CSV |
|
|
53
|
+
| **Dev experience** | TypeScript definitions, ESM/CJS support, CLI for quick runs |
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
const { backtest, getHistoricalCandles, ema } = require("tradelab");
|
|
49
|
-
const { fetchHistorical } = require("tradelab/data");
|
|
50
|
-
```
|
|
55
|
+
---
|
|
51
56
|
|
|
52
|
-
## Quick
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
If you already have candles, `backtest()` is the main entry point.
|
|
53
60
|
|
|
54
61
|
```js
|
|
55
62
|
import { backtest, ema, exportBacktestArtifacts } from "tradelab";
|
|
@@ -75,29 +82,23 @@ const result = backtest({
|
|
|
75
82
|
const risk = entry - stop;
|
|
76
83
|
if (risk <= 0) return null;
|
|
77
84
|
|
|
78
|
-
return {
|
|
79
|
-
side: "long",
|
|
80
|
-
entry,
|
|
81
|
-
stop,
|
|
82
|
-
rr: 2,
|
|
83
|
-
};
|
|
85
|
+
return { side: "long", entry, stop, rr: 2 };
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
return null;
|
|
87
89
|
},
|
|
88
90
|
});
|
|
89
91
|
|
|
90
|
-
exportBacktestArtifacts({
|
|
91
|
-
result,
|
|
92
|
-
outDir: "./output",
|
|
93
|
-
});
|
|
92
|
+
exportBacktestArtifacts({ result, outDir: "./output" });
|
|
94
93
|
```
|
|
95
94
|
|
|
96
|
-
|
|
95
|
+
After the run, check `result.metrics` for the headline numbers and `result.positions` for the trade log.
|
|
96
|
+
|
|
97
|
+
---
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
## Loading historical data
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
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
102
|
|
|
102
103
|
```js
|
|
103
104
|
import { getHistoricalCandles, backtest } from "tradelab";
|
|
@@ -107,138 +108,297 @@ const candles = await getHistoricalCandles({
|
|
|
107
108
|
symbol: "SPY",
|
|
108
109
|
interval: "1d",
|
|
109
110
|
period: "2y",
|
|
110
|
-
cache: true,
|
|
111
|
+
cache: true, // reuses local copy on repeated runs
|
|
111
112
|
});
|
|
112
113
|
|
|
113
|
-
const result = backtest({
|
|
114
|
-
candles,
|
|
115
|
-
symbol: "SPY",
|
|
116
|
-
interval: "1d",
|
|
117
|
-
range: "2y",
|
|
118
|
-
signal,
|
|
119
|
-
});
|
|
114
|
+
const result = backtest({ candles, symbol: "SPY", interval: "1d", range: "2y", signal });
|
|
120
115
|
```
|
|
121
116
|
|
|
122
|
-
Supported
|
|
117
|
+
**Supported sources:** `yahoo` · `csv` · `auto`
|
|
123
118
|
|
|
124
|
-
|
|
119
|
+
**Supported periods:** `5d` · `60d` · `6mo` · `1y` · `2y` · and more
|
|
125
120
|
|
|
126
|
-
|
|
127
|
-
import { getHistoricalCandles } from "tradelab";
|
|
121
|
+
Use `cache: true` for repeatable research runs. It eliminates network noise and makes failures easier to diagnose.
|
|
128
122
|
|
|
123
|
+
### CSV import
|
|
124
|
+
|
|
125
|
+
```js
|
|
129
126
|
const candles = await getHistoricalCandles({
|
|
130
127
|
source: "csv",
|
|
131
|
-
|
|
132
|
-
interval: "5m",
|
|
133
|
-
csvPath: "./data/btc-5m.csv",
|
|
128
|
+
csvPath: "./data/spy.csv",
|
|
134
129
|
csv: {
|
|
135
|
-
timeCol: "
|
|
130
|
+
timeCol: "timestamp",
|
|
136
131
|
openCol: "open",
|
|
137
|
-
|
|
138
|
-
lowCol: "low",
|
|
139
|
-
closeCol: "close",
|
|
140
|
-
volumeCol: "volume",
|
|
132
|
+
// ... optional column mapping
|
|
141
133
|
},
|
|
142
134
|
});
|
|
143
135
|
```
|
|
144
136
|
|
|
145
|
-
If
|
|
137
|
+
If your CSV already uses standard OHLCV column names, no mapping is needed at all.
|
|
146
138
|
|
|
147
|
-
|
|
139
|
+
---
|
|
148
140
|
|
|
149
|
-
|
|
141
|
+
## Core concepts
|
|
142
|
+
|
|
143
|
+
### The signal function
|
|
144
|
+
|
|
145
|
+
Your signal function is called on every bar. Return `null` to skip, or a signal object to open a trade.
|
|
150
146
|
|
|
151
147
|
```js
|
|
152
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
148
|
+
signal({ candles, index, bar, equity, openPosition, pendingOrder }) {
|
|
149
|
+
// return null to skip
|
|
150
|
+
// return a signal to enter
|
|
151
|
+
return {
|
|
152
|
+
side: "long", // "long" | "short" | "buy" | "sell"
|
|
153
|
+
entry: bar.close, // defaults to current close if omitted
|
|
154
|
+
stop: bar.close - 2,
|
|
155
|
+
rr: 2, // target = entry + (entry - stop) * rr
|
|
156
|
+
};
|
|
159
157
|
}
|
|
160
158
|
```
|
|
161
159
|
|
|
162
|
-
|
|
160
|
+
The minimum viable signal is just `side`, `stop`, and `rr`. Start there and add fields only when the strategy actually needs them.
|
|
161
|
+
|
|
162
|
+
### Key backtest options
|
|
163
|
+
|
|
164
|
+
| Option | Purpose |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `equity` | Starting equity (default `10000`) |
|
|
167
|
+
| `riskPct` | Percent of equity risked per trade |
|
|
168
|
+
| `warmupBars` | Bars skipped before signal evaluation starts |
|
|
169
|
+
| `flattenAtClose` | Forces end-of-day exit when enabled |
|
|
170
|
+
| `costs` | Slippage, spread, and commission model |
|
|
171
|
+
| `strict` | Throws on lookahead access |
|
|
172
|
+
| `collectEqSeries` | Enables equity curve output |
|
|
173
|
+
| `collectReplay` | Enables visualization payload |
|
|
174
|
+
|
|
175
|
+
### Result shape
|
|
163
176
|
|
|
164
177
|
```js
|
|
165
178
|
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
179
|
+
symbol, interval, range,
|
|
180
|
+
trades, // every realized leg, including partial exits
|
|
181
|
+
positions, // completed positions - start here for analysis
|
|
182
|
+
metrics, // winRate, profitFactor, maxDrawdown, sharpe, ...
|
|
183
|
+
eqSeries, // [{ time, timestamp, equity }] - equity curve
|
|
184
|
+
replay, // visualization frames and events
|
|
170
185
|
}
|
|
171
186
|
```
|
|
172
187
|
|
|
173
|
-
|
|
188
|
+
**First checks after any run:**
|
|
174
189
|
|
|
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
|
|
190
|
+
- `metrics.trades` - enough sample size to trust the numbers?
|
|
191
|
+
- `metrics.profitFactor` - do winners beat losers gross of costs?
|
|
192
|
+
- `metrics.maxDrawdown` - is the equity path survivable?
|
|
193
|
+
- `metrics.sideBreakdown` - does one side carry the whole result?
|
|
181
194
|
|
|
182
|
-
|
|
195
|
+
---
|
|
183
196
|
|
|
184
|
-
|
|
185
|
-
- `_cooldownBars`
|
|
186
|
-
- `_breakevenAtR`
|
|
187
|
-
- `_trailAfterR`
|
|
188
|
-
- `_maxBarsInTrade`
|
|
189
|
-
- `_maxHoldMin`
|
|
190
|
-
- `_rr`
|
|
191
|
-
- `_initRisk`
|
|
192
|
-
- `_imb`
|
|
197
|
+
## Portfolio mode
|
|
193
198
|
|
|
194
|
-
|
|
199
|
+
Use `backtestPortfolio()` when you have one candle array per symbol and want a single combined result.
|
|
195
200
|
|
|
196
|
-
|
|
201
|
+
```js
|
|
202
|
+
import { backtestPortfolio } from "tradelab";
|
|
203
|
+
|
|
204
|
+
const result = backtestPortfolio({
|
|
205
|
+
equity: 100_000,
|
|
206
|
+
systems: [
|
|
207
|
+
{ symbol: "SPY", candles: spy, signal: signalA, weight: 2 },
|
|
208
|
+
{ symbol: "QQQ", candles: qqq, signal: signalB, weight: 1 },
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
```
|
|
197
212
|
|
|
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
|
|
213
|
+
Weights now act as default per-system allocation caps rather than pre-funded sleeves. Capital is locked only when a fill happens, `eqSeries` includes `lockedCapital` and `availableCapital`, later systems size against remaining live capital, and `maxDailyLossPct` on `backtestPortfolio()` can halt the whole book for the rest of the day.
|
|
203
214
|
|
|
204
|
-
|
|
215
|
+
---
|
|
205
216
|
|
|
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 })`
|
|
217
|
+
## Walk-forward optimization
|
|
217
218
|
|
|
218
|
-
|
|
219
|
+
Use `walkForwardOptimize()` when one in-sample backtest is not enough. It supports rolling and anchored train/test windows across the full candle history.
|
|
219
220
|
|
|
220
|
-
|
|
221
|
+
```js
|
|
222
|
+
import { walkForwardOptimize } from "tradelab";
|
|
221
223
|
|
|
222
|
-
|
|
224
|
+
const wf = walkForwardOptimize({
|
|
225
|
+
candles,
|
|
226
|
+
mode: "anchored",
|
|
227
|
+
trainBars: 180,
|
|
228
|
+
testBars: 60,
|
|
229
|
+
stepBars: 60,
|
|
230
|
+
scoreBy: "profitFactor",
|
|
231
|
+
parameterSets: [
|
|
232
|
+
{ fast: 8, slow: 21, rr: 2 },
|
|
233
|
+
{ fast: 10, slow: 30, rr: 2 },
|
|
234
|
+
],
|
|
235
|
+
signalFactory(params) {
|
|
236
|
+
return createSignalFromParams(params);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
```
|
|
223
240
|
|
|
224
|
-
|
|
241
|
+
Each window picks the best parameter set in training, then runs it blind on the test slice. The `windows` array now includes out-of-sample trade count, profitability, and a per-window stability score. `bestParamsSummary` reports how stable the winners were across the full run.
|
|
225
242
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Tick backtests
|
|
246
|
+
|
|
247
|
+
Use `backtestTicks()` when you want event-driven fills on tick or quote data without changing the result shape used by metrics, exports, or replay.
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
import { backtestTicks } from "tradelab";
|
|
251
|
+
|
|
252
|
+
const result = backtestTicks({
|
|
253
|
+
ticks,
|
|
254
|
+
queueFillProbability: 0.35,
|
|
255
|
+
signal,
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Market entries fill on the next tick, limit orders can fill at the touch with configurable queue probability, and stop exits use the existing cost model with stop-specific slippage if you provide it in `costs.slippageByKind.stop`.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Execution and cost modeling
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
const result = backtest({
|
|
267
|
+
candles,
|
|
268
|
+
signal,
|
|
269
|
+
costs: {
|
|
270
|
+
slippageBps: 2,
|
|
271
|
+
spreadBps: 1,
|
|
272
|
+
slippageByKind: {
|
|
273
|
+
market: 3,
|
|
274
|
+
limit: 0.5,
|
|
275
|
+
stop: 4,
|
|
276
|
+
},
|
|
277
|
+
commissionBps: 1,
|
|
278
|
+
commissionPerUnit: 0,
|
|
279
|
+
commissionPerOrder: 1,
|
|
280
|
+
minCommission: 1,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
- Slippage is applied in the trade direction
|
|
286
|
+
- Spread is modeled as half-spread paid on entry and exit
|
|
287
|
+
- Commission can be percentage-based, per-unit, per-order, or mixed
|
|
288
|
+
- `minCommission` floors the fee per fill
|
|
289
|
+
|
|
290
|
+
> Leaving costs at zero is the most common cause of inflated backtests. Set them from the start.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Exports and reporting
|
|
295
|
+
|
|
296
|
+
```js
|
|
297
|
+
import { exportBacktestArtifacts } from "tradelab";
|
|
298
|
+
|
|
299
|
+
// Writes HTML report + trade CSV + metrics JSON in one call
|
|
300
|
+
exportBacktestArtifacts({ result, outDir: "./output" });
|
|
229
301
|
```
|
|
230
302
|
|
|
303
|
+
Or use the narrower helpers:
|
|
304
|
+
|
|
305
|
+
| Helper | Output |
|
|
306
|
+
|---|---|
|
|
307
|
+
| `exportHtmlReport(options)` | Interactive HTML report written to disk |
|
|
308
|
+
| `renderHtmlReport(options)` | HTML report returned as a string |
|
|
309
|
+
| `exportTradesCsv(trades, options)` | Flat trade ledger for spreadsheets or pandas |
|
|
310
|
+
| `exportMetricsJSON(options)` | Machine-readable metrics for dashboards or automation |
|
|
311
|
+
|
|
312
|
+
For programmatic pipelines, `exportMetricsJSON` is usually the most useful format to build on.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
231
316
|
## CLI
|
|
232
317
|
|
|
318
|
+
The package ships a `tradelab` binary. Best for quick iteration, smoke tests, and trying the package before wiring it into application code.
|
|
319
|
+
|
|
233
320
|
```bash
|
|
321
|
+
# Backtest from Yahoo
|
|
234
322
|
npx tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
|
|
323
|
+
|
|
324
|
+
# Backtest from CSV with a built-in strategy
|
|
235
325
|
npx tradelab backtest --source csv --csvPath ./data/btc.csv --strategy buy-hold --holdBars 3
|
|
236
|
-
|
|
326
|
+
|
|
327
|
+
# Multi-symbol portfolio
|
|
328
|
+
npx tradelab portfolio \
|
|
329
|
+
--csvPaths ./data/spy.csv,./data/qqq.csv \
|
|
330
|
+
--symbols SPY,QQQ \
|
|
331
|
+
--strategy buy-hold
|
|
332
|
+
|
|
333
|
+
# Walk-forward validation
|
|
334
|
+
npx tradelab walk-forward \
|
|
335
|
+
--source yahoo --symbol QQQ --interval 1d --period 2y \
|
|
336
|
+
--trainBars 180 --testBars 60 --mode anchored
|
|
337
|
+
|
|
338
|
+
# Prefetch and cache data
|
|
339
|
+
npx tradelab prefetch --symbol SPY --interval 1d --period 1y
|
|
340
|
+
npx tradelab import-csv --csvPath ./data/spy.csv --symbol SPY --interval 1d
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Built-in strategies:** `ema-cross` · `buy-hold`
|
|
344
|
+
|
|
345
|
+
You can also point `--strategy` at a local module that exports `default(args)`, `createSignal(args)`, or `signal` for `backtest`, or `signalFactory(params, args)` plus `parameterSets`/`createParameterSets(args)` for `walk-forward`.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Examples
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
node examples/emaCross.js
|
|
353
|
+
node examples/yahooEmaCross.js SPY 1d 1y
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
The examples are a good place to start if you want something runnable before wiring the package into your own strategy code.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Importing
|
|
361
|
+
|
|
362
|
+
### ESM
|
|
363
|
+
|
|
364
|
+
```js
|
|
365
|
+
import { backtest, getHistoricalCandles, ema } from "tradelab";
|
|
366
|
+
import { fetchHistorical } from "tradelab/data";
|
|
237
367
|
```
|
|
238
368
|
|
|
369
|
+
### CommonJS
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
const { backtest, getHistoricalCandles, ema } = require("tradelab");
|
|
373
|
+
const { fetchHistorical } = require("tradelab/data");
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Documentation
|
|
379
|
+
|
|
380
|
+
| Guide | What it covers |
|
|
381
|
+
|---|---|
|
|
382
|
+
| [Backtest engine](docs/backtest-engine.md) | Signal contract, all options, result shape, portfolio mode, walk-forward |
|
|
383
|
+
| [Data, reporting, and CLI](docs/data-reporting-cli.md) | Data loading, cache behavior, exports, CLI reference |
|
|
384
|
+
| [Strategy examples](docs/examples.md) | Mean reversion, breakout, sentiment, LLM, and portfolio strategy patterns |
|
|
385
|
+
| [API reference](docs/api-reference.md) | Compact index of every public export |
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Common mistakes
|
|
390
|
+
|
|
391
|
+
- Using unsorted candles or mixed intervals in a single series
|
|
392
|
+
- Reading `trades` as if they were always full positions - use `positions` for top-line analysis
|
|
393
|
+
- Leaving costs at zero and overestimating edge
|
|
394
|
+
- Trusting one backtest without out-of-sample validation
|
|
395
|
+
- Debugging a strategy with `strict: false` when lookahead is possible
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
239
399
|
## Notes
|
|
240
400
|
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
244
|
-
-
|
|
401
|
+
- Node `18+` is required
|
|
402
|
+
- Yahoo downloads are cached under `output/data` by default
|
|
403
|
+
- CommonJS and ESM are both supported
|
|
404
|
+
- The engine is built for historical research - not brokerage execution or full exchange microstructure simulation
|
package/bin/tradelab.js
CHANGED
|
@@ -48,6 +48,11 @@ function toList(value, fallback) {
|
|
|
48
48
|
.filter((item) => Number.isFinite(item));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function parseJsonValue(value, fallback = null) {
|
|
52
|
+
if (!value) return fallback;
|
|
53
|
+
return JSON.parse(String(value));
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
function createEmaCrossSignal({
|
|
52
57
|
fast = 10,
|
|
53
58
|
slow = 30,
|
|
@@ -133,13 +138,70 @@ async function loadStrategy(strategyArg, args) {
|
|
|
133
138
|
throw new Error(`Strategy module "${strategyArg}" must export default, createSignal, or signal`);
|
|
134
139
|
}
|
|
135
140
|
|
|
141
|
+
async function loadWalkForwardStrategy(strategyArg, args) {
|
|
142
|
+
if (!strategyArg || strategyArg === "ema-cross") {
|
|
143
|
+
const fasts = toList(args.fasts, [8, 10, 12]);
|
|
144
|
+
const slows = toList(args.slows, [20, 30, 40]);
|
|
145
|
+
const rrs = toList(args.rrs, [1.5, 2, 3]);
|
|
146
|
+
const parameterSets = [];
|
|
147
|
+
|
|
148
|
+
for (const fast of fasts) {
|
|
149
|
+
for (const slow of slows) {
|
|
150
|
+
if (fast >= slow) continue;
|
|
151
|
+
for (const rr of rrs) {
|
|
152
|
+
parameterSets.push({ fast, slow, rr });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
parameterSets,
|
|
159
|
+
signalFactory(params) {
|
|
160
|
+
return createEmaCrossSignal({
|
|
161
|
+
fast: params.fast,
|
|
162
|
+
slow: params.slow,
|
|
163
|
+
rr: params.rr,
|
|
164
|
+
stopLookback: toNumber(args.stopLookback, 15),
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const resolved = path.resolve(process.cwd(), strategyArg);
|
|
171
|
+
const module = await import(pathToFileURL(resolved).href);
|
|
172
|
+
if (typeof module.signalFactory !== "function") {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Walk-forward strategy module "${strategyArg}" must export signalFactory`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const parameterSets =
|
|
179
|
+
parseJsonValue(args.parameterSets) ??
|
|
180
|
+
(typeof module.createParameterSets === "function"
|
|
181
|
+
? await module.createParameterSets(args)
|
|
182
|
+
: module.parameterSets);
|
|
183
|
+
|
|
184
|
+
if (!Array.isArray(parameterSets) || parameterSets.length === 0) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Walk-forward strategy module "${strategyArg}" must provide parameterSets, createParameterSets(args), or --parameterSets`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
parameterSets,
|
|
192
|
+
signalFactory(params) {
|
|
193
|
+
return module.signalFactory(params, args);
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
136
198
|
function printHelp() {
|
|
137
199
|
console.log(`tradelab
|
|
138
200
|
|
|
139
201
|
Commands:
|
|
140
202
|
backtest Run a one-off backtest from Yahoo or CSV data
|
|
141
203
|
portfolio Run multiple CSV datasets as an equal-weight portfolio
|
|
142
|
-
walk-forward Run rolling train/test optimization
|
|
204
|
+
walk-forward Run rolling or anchored train/test optimization
|
|
143
205
|
prefetch Download Yahoo candles into the local cache
|
|
144
206
|
import-csv Normalize a CSV and save it into the local cache
|
|
145
207
|
|
|
@@ -257,26 +319,15 @@ async function commandWalkForward(args) {
|
|
|
257
319
|
csvPath: args.csvPath,
|
|
258
320
|
cache: args.cache !== "false",
|
|
259
321
|
});
|
|
260
|
-
const
|
|
261
|
-
const slows = toList(args.slows, [20, 30, 40]);
|
|
262
|
-
const rrs = toList(args.rrs, [1.5, 2, 3]);
|
|
263
|
-
const parameterSets = [];
|
|
264
|
-
|
|
265
|
-
for (const fast of fasts) {
|
|
266
|
-
for (const slow of slows) {
|
|
267
|
-
if (fast >= slow) continue;
|
|
268
|
-
for (const rr of rrs) {
|
|
269
|
-
parameterSets.push({ fast, slow, rr });
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
322
|
+
const walkForwardStrategy = await loadWalkForwardStrategy(args.strategy, args);
|
|
273
323
|
|
|
274
324
|
const result = walkForwardOptimize({
|
|
275
325
|
candles,
|
|
276
|
-
parameterSets,
|
|
326
|
+
parameterSets: walkForwardStrategy.parameterSets,
|
|
277
327
|
trainBars: toNumber(args.trainBars, 120),
|
|
278
328
|
testBars: toNumber(args.testBars, 40),
|
|
279
329
|
stepBars: toNumber(args.stepBars, toNumber(args.testBars, 40)),
|
|
330
|
+
mode: args.mode || "rolling",
|
|
280
331
|
scoreBy: args.scoreBy || "profitFactor",
|
|
281
332
|
backtestOptions: {
|
|
282
333
|
symbol: args.symbol || "DATA",
|
|
@@ -286,14 +337,7 @@ async function commandWalkForward(args) {
|
|
|
286
337
|
riskPct: toNumber(args.riskPct, 1),
|
|
287
338
|
warmupBars: toNumber(args.warmupBars, 20),
|
|
288
339
|
},
|
|
289
|
-
signalFactory
|
|
290
|
-
return createEmaCrossSignal({
|
|
291
|
-
fast: params.fast,
|
|
292
|
-
slow: params.slow,
|
|
293
|
-
rr: params.rr,
|
|
294
|
-
stopLookback: toNumber(args.stopLookback, 15),
|
|
295
|
-
});
|
|
296
|
-
},
|
|
340
|
+
signalFactory: walkForwardStrategy.signalFactory,
|
|
297
341
|
});
|
|
298
342
|
|
|
299
343
|
const metricsPath = exportMetricsJSON({
|
|
@@ -310,6 +354,7 @@ async function commandWalkForward(args) {
|
|
|
310
354
|
windows: result.windows.length,
|
|
311
355
|
positions: result.positions.length,
|
|
312
356
|
finalEquity: result.metrics.finalEquity,
|
|
357
|
+
bestParamsSummary: result.bestParamsSummary,
|
|
313
358
|
metricsPath,
|
|
314
359
|
},
|
|
315
360
|
null,
|