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 CHANGED
@@ -1,55 +1,61 @@
1
- <img src="https://i.imgur.com/HGvvQbq.png" width="500" alt="tradelab logo"/>
1
+ <div align="center">
2
+ <img src="https://i.imgur.com/HGvvQbq.png" width="420" alt="tradelab logo" />
2
3
 
3
- # tradelab
4
+ <p><strong>A Node.js backtesting toolkit for serious trading strategy research.</strong></p>
4
5
 
5
- `tradelab` is a Node.js backtesting toolkit for trading strategy research. It lets you:
6
- - load candles from Yahoo Finance or CSV
7
- - run candle-based backtests with sizing, exits, and risk controls
8
- - export trades, metrics, and HTML reports
6
+ [![npm version](https://img.shields.io/npm/v/tradelab?color=0f172a&label=npm&logo=npm)](https://www.npmjs.com/package/tradelab)
7
+ [![GitHub](https://img.shields.io/badge/github-ishsharm0/tradelab-0f172a?logo=github)](https://github.com/ishsharm0/tradelab)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-0f172a)](https://github.com/ishsharm0/tradelab/blob/main/LICENSE)
9
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-0f172a?logo=node.js)](https://nodejs.org)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-ready-0f172a?logo=typescript)](https://github.com/ishsharm0/tradelab/blob/main/types/index.d.ts)
9
11
 
10
- The package is modular by design, so you can use just the parts you need: data loading, backtesting, reporting, or the utility layer on its own.
12
+ </div>
11
13
 
12
- It is built for historical research and testing, not broker connectivity or live trading.
14
+ ---
13
15
 
14
- ## Features
16
+ **tradelab** handles the simulation, sizing, exits, costs, and result exports — you bring the candles and signal logic.
15
17
 
16
- - Modular structure: use the full workflow or just the engine, data layer, reporting, or helpers
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
- Node `18+` is required.
24
+ ---
34
25
 
35
- ## Importing
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
- ### ESM (recommended)
40
+ ---
39
41
 
40
- ```js
41
- import { backtest, getHistoricalCandles, ema } from "tradelab";
42
- import { fetchHistorical } from "tradelab/data";
43
- ```
42
+ ## What it includes
44
43
 
45
- ### CommonJS
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
- ```js
48
- const { backtest, getHistoricalCandles, ema } = require("tradelab");
49
- const { fetchHistorical } = require("tradelab/data");
50
- ```
54
+ ---
55
+
56
+ ## Quick start
51
57
 
52
- ## Quick Start
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
- ## Getting Historical Data
94
+ After the run, check `result.metrics` for the headline numbers and `result.positions` for the trade log.
97
95
 
98
- The simplest entry point is `getHistoricalCandles()`. For most users, it is the only data-loading function you need.
96
+ ---
99
97
 
100
- ### Yahoo Finance
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 period examples: `5d`, `60d`, `6mo`, `1y`.
116
+ **Supported sources:** `yahoo` · `csv` · `auto`
123
117
 
124
- ### CSV
118
+ **Supported periods:** `5d` · `60d` · `6mo` · `1y` · `2y` · and more
125
119
 
126
- ```js
127
- import { getHistoricalCandles } from "tradelab";
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
- symbol: "BTC-USD",
132
- interval: "5m",
133
- csvPath: "./data/btc-5m.csv",
127
+ csvPath: "./data/spy.csv",
134
128
  csv: {
135
- timeCol: "time",
129
+ timeCol: "timestamp",
136
130
  openCol: "open",
137
- highCol: "high",
138
- lowCol: "low",
139
- closeCol: "close",
140
- volumeCol: "volume",
131
+ // ... optional column mapping
141
132
  },
142
133
  });
143
134
  ```
144
135
 
145
- If you pass `csvPath` and omit `source`, the loader will auto-detect CSV mode.
136
+ If your CSV already uses standard OHLCV column names, no mapping is needed at all.
137
+
138
+ ---
146
139
 
147
- ## Signal Contract
140
+ ## Core concepts
148
141
 
149
- Your strategy function receives:
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
- candles, // history through the current bar
154
- index, // current index in the original candle array
155
- bar, // current candle
156
- equity, // realized equity
157
- openPosition, // null or current position
158
- pendingOrder // null or current pending entry
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
- Return `null` for no trade, or a signal object:
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
- side: "long" | "short",
167
- entry: Number,
168
- stop: Number,
169
- takeProfit: Number
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
- Quality-of-life behavior:
187
+ **First checks after any run:**
174
188
 
175
- - `side` also accepts `buy` and `sell`
176
- - `entry` can be omitted and will default to the current bar close
177
- - `takeProfit` can be omitted if `rr` or `_rr` is provided
178
- - `qty` or `size` can override risk-based sizing
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
- Optional engine hints:
194
+ ---
183
195
 
184
- - `_entryExpiryBars`
185
- - `_cooldownBars`
186
- - `_breakevenAtR`
187
- - `_trailAfterR`
188
- - `_maxBarsInTrade`
189
- - `_maxHoldMin`
190
- - `_rr`
191
- - `_initRisk`
192
- - `_imb`
196
+ ## Portfolio mode
193
197
 
194
- ## Result Shape
198
+ Use `backtestPortfolio()` when you have one candle array per symbol and want a single combined result.
195
199
 
196
- `backtest()` returns:
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
- - `trades`: every realized leg, including scale-outs
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
- ## Main Exports
214
+ ---
205
215
 
206
- - `backtest(options)`
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
- ## Reports
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
- The HTML report is self-contained apart from the Plotly CDN script. Report markup, CSS, and client-side chart code live under `templates/`.
220
+ ```js
221
+ import { walkForwardOptimize } from "tradelab";
221
222
 
222
- Export helpers default CSV output to completed positions. Use `csvSource: "trades"` if you want every realized leg in the CSV.
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
- ## Examples
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
- ```bash
227
- node examples/emaCross.js
228
- node examples/yahooEmaCross.js SPY 1d 1y
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
- npx tradelab walk-forward --source yahoo --symbol QQQ --interval 1d --period 2y --trainBars 180 --testBars 60
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
- - Yahoo downloads can be cached under `output/data` by default.
242
- - The engine is intended for historical research, not brokerage execution.
243
- - File output only happens through the reporting and cache helpers.
244
- - CommonJS and ESM are both supported.
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.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",