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 CHANGED
@@ -1,55 +1,62 @@
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 data 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
+ - [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
- ### ESM (recommended)
41
+ ---
39
42
 
40
- ```js
41
- import { backtest, getHistoricalCandles, ema } from "tradelab";
42
- import { fetchHistorical } from "tradelab/data";
43
- ```
43
+ ## What it includes
44
44
 
45
- ### CommonJS
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
- ```js
48
- const { backtest, getHistoricalCandles, ema } = require("tradelab");
49
- const { fetchHistorical } = require("tradelab/data");
50
- ```
55
+ ---
51
56
 
52
- ## Quick Start
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
- ## Getting Historical Data
95
+ After the run, check `result.metrics` for the headline numbers and `result.positions` for the trade log.
96
+
97
+ ---
97
98
 
98
- The simplest entry point is `getHistoricalCandles()`. For most users, it is the only data-loading function you need.
99
+ ## Loading historical data
99
100
 
100
- ### Yahoo Finance
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 period examples: `5d`, `60d`, `6mo`, `1y`.
117
+ **Supported sources:** `yahoo` · `csv` · `auto`
123
118
 
124
- ### CSV
119
+ **Supported periods:** `5d` · `60d` · `6mo` · `1y` · `2y` · and more
125
120
 
126
- ```js
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
- symbol: "BTC-USD",
132
- interval: "5m",
133
- csvPath: "./data/btc-5m.csv",
128
+ csvPath: "./data/spy.csv",
134
129
  csv: {
135
- timeCol: "time",
130
+ timeCol: "timestamp",
136
131
  openCol: "open",
137
- highCol: "high",
138
- lowCol: "low",
139
- closeCol: "close",
140
- volumeCol: "volume",
132
+ // ... optional column mapping
141
133
  },
142
134
  });
143
135
  ```
144
136
 
145
- If you pass `csvPath` and omit `source`, the loader will auto-detect CSV mode.
137
+ If your CSV already uses standard OHLCV column names, no mapping is needed at all.
146
138
 
147
- ## Signal Contract
139
+ ---
148
140
 
149
- Your strategy function receives:
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
- 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
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
- Return `null` for no trade, or a signal object:
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
- side: "long" | "short",
167
- entry: Number,
168
- stop: Number,
169
- takeProfit: Number
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
- Quality-of-life behavior:
188
+ **First checks after any run:**
174
189
 
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
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
- Optional engine hints:
195
+ ---
183
196
 
184
- - `_entryExpiryBars`
185
- - `_cooldownBars`
186
- - `_breakevenAtR`
187
- - `_trailAfterR`
188
- - `_maxBarsInTrade`
189
- - `_maxHoldMin`
190
- - `_rr`
191
- - `_initRisk`
192
- - `_imb`
197
+ ## Portfolio mode
193
198
 
194
- ## Result Shape
199
+ Use `backtestPortfolio()` when you have one candle array per symbol and want a single combined result.
195
200
 
196
- `backtest()` returns:
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
- - `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
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
- ## Main Exports
215
+ ---
205
216
 
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 })`
217
+ ## Walk-forward optimization
217
218
 
218
- ## Reports
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
- The HTML report is self-contained apart from the Plotly CDN script. Report markup, CSS, and client-side chart code live under `templates/`.
221
+ ```js
222
+ import { walkForwardOptimize } from "tradelab";
221
223
 
222
- Export helpers default CSV output to completed positions. Use `csvSource: "trades"` if you want every realized leg in the CSV.
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
- ## Examples
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
- ```bash
227
- node examples/emaCross.js
228
- node examples/yahooEmaCross.js SPY 1d 1y
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
- npx tradelab walk-forward --source yahoo --symbol QQQ --interval 1d --period 2y --trainBars 180 --testBars 60
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
- - 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.
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 with the built-in ema-cross strategy
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 fasts = toList(args.fasts, [8, 10, 12]);
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(params) {
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,