tradelab 0.2.0 → 0.4.0

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