tradelab 0.5.0 → 1.0.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.
Files changed (54) hide show
  1. package/README.md +89 -41
  2. package/bin/tradelab.js +276 -30
  3. package/dist/cjs/data.cjs +134 -104
  4. package/dist/cjs/index.cjs +378 -177
  5. package/dist/cjs/live.cjs +3350 -0
  6. package/docs/README.md +21 -9
  7. package/docs/api-reference.md +87 -29
  8. package/docs/backtest-engine.md +37 -53
  9. package/docs/data-reporting-cli.md +60 -34
  10. package/docs/examples.md +6 -12
  11. package/docs/live-trading.md +186 -0
  12. package/examples/yahooEmaCross.js +1 -6
  13. package/package.json +18 -3
  14. package/src/data/csv.js +24 -14
  15. package/src/data/index.js +1 -5
  16. package/src/data/yahoo.js +6 -19
  17. package/src/engine/backtest.js +137 -144
  18. package/src/engine/backtestTicks.js +89 -37
  19. package/src/engine/barSystemRunner.js +182 -118
  20. package/src/engine/execution.js +11 -39
  21. package/src/engine/portfolio.js +54 -6
  22. package/src/engine/walkForward.js +37 -14
  23. package/src/index.js +2 -11
  24. package/src/live/broker/alpaca.js +254 -0
  25. package/src/live/broker/binance.js +351 -0
  26. package/src/live/broker/coinbase.js +339 -0
  27. package/src/live/broker/interactiveBrokers.js +123 -0
  28. package/src/live/broker/interface.js +74 -0
  29. package/src/live/clock.js +56 -0
  30. package/src/live/engine/candleAggregator.js +154 -0
  31. package/src/live/engine/liveEngine.js +694 -0
  32. package/src/live/engine/paperEngine.js +453 -0
  33. package/src/live/engine/riskManager.js +185 -0
  34. package/src/live/engine/stateManager.js +112 -0
  35. package/src/live/events.js +48 -0
  36. package/src/live/feed/brokerFeed.js +35 -0
  37. package/src/live/feed/interface.js +28 -0
  38. package/src/live/feed/pollingFeed.js +105 -0
  39. package/src/live/index.js +27 -0
  40. package/src/live/logger.js +82 -0
  41. package/src/live/orchestrator.js +133 -0
  42. package/src/live/storage/interface.js +36 -0
  43. package/src/live/storage/jsonFileStorage.js +112 -0
  44. package/src/metrics/buildMetrics.js +18 -41
  45. package/src/reporting/exportBacktestArtifacts.js +1 -4
  46. package/src/reporting/exportTradesCsv.js +2 -7
  47. package/src/reporting/renderHtmlReport.js +8 -13
  48. package/src/utils/indicators.js +1 -2
  49. package/src/utils/positionSizing.js +16 -2
  50. package/src/utils/time.js +4 -12
  51. package/templates/report.html +23 -9
  52. package/templates/report.js +83 -69
  53. package/types/index.d.ts +21 -3
  54. package/types/live.d.ts +382 -0
package/README.md CHANGED
@@ -3,19 +3,19 @@
3
3
 
4
4
  <p><strong>A Node.js backtesting toolkit for serious trading strategy research.</strong></p>
5
5
 
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)
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)
11
11
 
12
12
  </div>
13
13
 
14
14
  ---
15
15
 
16
- **tradelab** handles the simulation, sizing, exits, costs, and result exports; you bring the data and signal logic.
16
+ **tradelab** handles strategy research and execution workflows in one package.
17
17
 
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.
18
+ Use it for backtests, portfolio and walk-forward validation, and live or paper execution through broker adapters while keeping the same `signal()` contract.
19
19
 
20
20
  ```bash
21
21
  npm install tradelab
@@ -32,6 +32,7 @@ npm install tradelab
32
32
  - [Portfolio mode](#portfolio-mode)
33
33
  - [Walk-forward optimization](#walk-forward-optimization)
34
34
  - [Tick backtests](#tick-backtests)
35
+ - [Live trading](#live-trading)
35
36
  - [Execution and cost modeling](#execution-and-cost-modeling)
36
37
  - [Exports and reporting](#exports-and-reporting)
37
38
  - [CLI](#cli)
@@ -42,15 +43,16 @@ npm install tradelab
42
43
 
43
44
  ## What it includes
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
+ | Area | What you get |
47
+ | ------------------ | ---------------------------------------------------------------------------------------- |
48
+ | **Engine** | Candle and tick backtests with position sizing, exits, replay capture, and cost models |
49
+ | **Portfolio** | Multi-system shared-capital simulation with live capital locking and daily loss halts |
50
+ | **Walk-forward** | Rolling and anchored train/test validation with parameter search and stability summaries |
51
+ | **Live execution** | Live and paper engines with broker adapters, state persistence, and orchestration |
52
+ | **Data** | Yahoo Finance downloads, CSV import, and local cache helpers |
53
+ | **Costs** | Slippage, spread, and commission modeling |
54
+ | **Exports** | HTML reports, metrics JSON, and trade CSV |
55
+ | **Dev experience** | TypeScript definitions, ESM/CJS support, CLI for quick runs |
54
56
 
55
57
  ---
56
58
 
@@ -108,7 +110,7 @@ const candles = await getHistoricalCandles({
108
110
  symbol: "SPY",
109
111
  interval: "1d",
110
112
  period: "2y",
111
- cache: true, // reuses local copy on repeated runs
113
+ cache: true, // reuses local copy on repeated runs
112
114
  });
113
115
 
114
116
  const result = backtest({ candles, symbol: "SPY", interval: "1d", range: "2y", signal });
@@ -161,16 +163,16 @@ The minimum viable signal is just `side`, `stop`, and `rr`. Start there and add
161
163
 
162
164
  ### Key backtest options
163
165
 
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 |
166
+ | Option | Purpose |
167
+ | ----------------- | -------------------------------------------- |
168
+ | `equity` | Starting equity (default `10000`) |
169
+ | `riskPct` | Percent of equity risked per trade |
170
+ | `warmupBars` | Bars skipped before signal evaluation starts |
171
+ | `flattenAtClose` | Forces end-of-day exit when enabled |
172
+ | `costs` | Slippage, spread, and commission model |
173
+ | `strict` | Throws on lookahead access |
174
+ | `collectEqSeries` | Enables equity curve output |
175
+ | `collectReplay` | Enables visualization payload |
174
176
 
175
177
  ### Result shape
176
178
 
@@ -229,7 +231,7 @@ const wf = walkForwardOptimize({
229
231
  stepBars: 60,
230
232
  scoreBy: "profitFactor",
231
233
  parameterSets: [
232
- { fast: 8, slow: 21, rr: 2 },
234
+ { fast: 8, slow: 21, rr: 2 },
233
235
  { fast: 10, slow: 30, rr: 2 },
234
236
  ],
235
237
  signalFactory(params) {
@@ -260,6 +262,40 @@ Market entries fill on the next tick, limit orders can fill at the touch with co
260
262
 
261
263
  ---
262
264
 
265
+ ## Live trading
266
+
267
+ `tradelab/live` provides the live stack:
268
+
269
+ - `LiveEngine` for single-system live/paper execution
270
+ - `LiveOrchestrator` for multi-system execution with shared broker state
271
+ - `PaperEngine` implementing the broker interface for deterministic simulation
272
+ - broker adapters for Alpaca, Binance, Coinbase, and Interactive Brokers
273
+ - JSON state/trade/equity persistence via `JsonFileStorage`
274
+
275
+ Use the same signal contract from backtesting in live mode:
276
+
277
+ ```js
278
+ import { LiveEngine, PaperEngine, JsonFileStorage } from "tradelab/live";
279
+
280
+ const engine = new LiveEngine({
281
+ id: "aapl-1m",
282
+ symbol: "AAPL",
283
+ interval: "1m",
284
+ broker: new PaperEngine({ equity: 25_000 }),
285
+ storage: new JsonFileStorage({ baseDir: "./output/live-state" }),
286
+ signal({ bar, openPosition }) {
287
+ if (openPosition) return null;
288
+ return { side: "long", stop: bar.close - 1, rr: 2 };
289
+ },
290
+ });
291
+
292
+ await engine.start();
293
+ ```
294
+
295
+ See [docs/live-trading.md](docs/live-trading.md) for API and CLI workflows.
296
+
297
+ ---
298
+
263
299
  ## Execution and cost modeling
264
300
 
265
301
  ```js
@@ -302,12 +338,12 @@ exportBacktestArtifacts({ result, outDir: "./output" });
302
338
 
303
339
  Or use the narrower helpers:
304
340
 
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 |
341
+ | Helper | Output |
342
+ | ---------------------------------- | ----------------------------------------------------- |
343
+ | `exportHtmlReport(options)` | Interactive HTML report written to disk |
344
+ | `renderHtmlReport(options)` | HTML report returned as a string |
345
+ | `exportTradesCsv(trades, options)` | Flat trade ledger for spreadsheets or pandas |
346
+ | `exportMetricsJSON(options)` | Machine-readable metrics for dashboards or automation |
311
347
 
312
348
  For programmatic pipelines, `exportMetricsJSON` is usually the most useful format to build on.
313
349
 
@@ -335,6 +371,15 @@ npx tradelab walk-forward \
335
371
  --source yahoo --symbol QQQ --interval 1d --period 2y \
336
372
  --trainBars 180 --testBars 60 --mode anchored
337
373
 
374
+ # Live paper engine (single system)
375
+ npx tradelab paper --symbol AAPL --interval 1m --mode polling --once true
376
+
377
+ # Live orchestrator from config
378
+ npx tradelab live --config ./live-portfolio.json --paper --mode polling --once true
379
+
380
+ # Inspect persisted live state
381
+ npx tradelab status --dir ./output/live-state
382
+
338
383
  # Prefetch and cache data
339
384
  npx tradelab prefetch --symbol SPY --interval 1d --period 1y
340
385
  npx tradelab import-csv --csvPath ./data/spy.csv --symbol SPY --interval 1d
@@ -364,6 +409,7 @@ The examples are a good place to start if you want something runnable before wir
364
409
  ```js
365
410
  import { backtest, getHistoricalCandles, ema } from "tradelab";
366
411
  import { fetchHistorical } from "tradelab/data";
412
+ import { LiveEngine, PaperEngine } from "tradelab/live";
367
413
  ```
368
414
 
369
415
  ### CommonJS
@@ -371,18 +417,20 @@ import { fetchHistorical } from "tradelab/data";
371
417
  ```js
372
418
  const { backtest, getHistoricalCandles, ema } = require("tradelab");
373
419
  const { fetchHistorical } = require("tradelab/data");
420
+ const { LiveEngine, PaperEngine } = require("tradelab/live");
374
421
  ```
375
422
 
376
423
  ---
377
424
 
378
425
  ## Documentation
379
426
 
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 |
427
+ | Guide | What it covers |
428
+ | ------------------------------------------------------ | ------------------------------------------------------------------------------ |
429
+ | [Backtest engine](docs/backtest-engine.md) | Signal contract, all options, result shape, portfolio mode, walk-forward |
430
+ | [Data, reporting, and CLI](docs/data-reporting-cli.md) | Data loading, cache behavior, exports, CLI reference |
431
+ | [Live trading](docs/live-trading.md) | Live engine, broker adapters, paper mode, orchestration, and state persistence |
432
+ | [Strategy examples](docs/examples.md) | Mean reversion, breakout, sentiment, LLM, and portfolio strategy patterns |
433
+ | [API reference](docs/api-reference.md) | Compact index of every public export |
386
434
 
387
435
  ---
388
436
 
@@ -401,4 +449,4 @@ const { fetchHistorical } = require("tradelab/data");
401
449
  - Node `18+` is required
402
450
  - Yahoo downloads are cached under `output/data` by default
403
451
  - CommonJS and ESM are both supported
404
- - The engine is built for historical research - not brokerage execution or full exchange microstructure simulation
452
+ - Live adapters support broker execution workflows, but this is still not an exchange microstructure simulator
package/bin/tradelab.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "node:fs";
2
3
  import path from "path";
3
4
  import { pathToFileURL } from "url";
4
5
 
@@ -13,6 +14,16 @@ import {
13
14
  saveCandlesToCache,
14
15
  walkForwardOptimize,
15
16
  } from "../src/index.js";
17
+ import {
18
+ AlpacaBroker,
19
+ BinanceBroker,
20
+ CoinbaseBroker,
21
+ InteractiveBrokersBroker,
22
+ JsonFileStorage,
23
+ LiveEngine,
24
+ LiveOrchestrator,
25
+ PaperEngine,
26
+ } from "../src/live/index.js";
16
27
 
17
28
  function parseArgs(argv) {
18
29
  const args = { _: [] };
@@ -24,12 +35,15 @@ function parseArgs(argv) {
24
35
  }
25
36
 
26
37
  const key = token.slice(2);
38
+ const camelKey = key.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
27
39
  const next = argv[index + 1];
40
+ const value = next && !next.startsWith("--") ? next : true;
41
+ args[key] = value;
42
+ if (camelKey !== key && args[camelKey] === undefined) {
43
+ args[camelKey] = value;
44
+ }
28
45
  if (next && !next.startsWith("--")) {
29
- args[key] = next;
30
46
  index += 1;
31
- } else {
32
- args[key] = true;
33
47
  }
34
48
  }
35
49
  return args;
@@ -50,15 +64,71 @@ function toList(value, fallback) {
50
64
 
51
65
  function parseJsonValue(value, fallback = null) {
52
66
  if (!value) return fallback;
53
- return JSON.parse(String(value));
67
+ try {
68
+ return JSON.parse(String(value));
69
+ } catch {
70
+ throw new Error(`Invalid JSON value: ${String(value).slice(0, 120)}`);
71
+ }
54
72
  }
55
73
 
56
- function createEmaCrossSignal({
57
- fast = 10,
58
- slow = 30,
59
- rr = 2,
60
- stopLookback = 15,
61
- } = {}) {
74
+ function toBoolean(value, fallback = false) {
75
+ if (value === undefined || value === null) return fallback;
76
+ if (value === true || value === false) return value;
77
+ const normalized = String(value).trim().toLowerCase();
78
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
79
+ if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
80
+ return fallback;
81
+ }
82
+
83
+ function loadJsonFile(filePath) {
84
+ const resolved = path.resolve(process.cwd(), filePath);
85
+ const raw = fs.readFileSync(resolved, "utf8");
86
+ return JSON.parse(raw);
87
+ }
88
+
89
+ function resolveBrokerName(name, paperMode = false) {
90
+ if (paperMode) return "paper";
91
+ return String(name || "paper").toLowerCase();
92
+ }
93
+
94
+ function createBrokerAdapter(args, overrides = {}) {
95
+ const brokerName = resolveBrokerName(
96
+ overrides.broker || args.broker,
97
+ toBoolean(overrides.paper ?? args.paper, false)
98
+ );
99
+
100
+ if (brokerName === "paper") {
101
+ return new PaperEngine({
102
+ equity: toNumber(overrides.equity ?? args.equity, 10_000),
103
+ slippageBps: toNumber(overrides.slippageBps ?? args.slippageBps, 0),
104
+ feeBps: toNumber(overrides.feeBps ?? args.feeBps, 0),
105
+ costs: parseJsonValue(overrides.costs ?? args.costs, null),
106
+ });
107
+ }
108
+
109
+ if (brokerName === "alpaca") return new AlpacaBroker();
110
+ if (brokerName === "binance") return new BinanceBroker();
111
+ if (brokerName === "coinbase") return new CoinbaseBroker();
112
+ if (brokerName === "ib" || brokerName === "interactivebrokers") {
113
+ return new InteractiveBrokersBroker();
114
+ }
115
+
116
+ throw new Error(`Unsupported broker "${brokerName}"`);
117
+ }
118
+
119
+ function brokerConfigFromArgs(args, overrides = {}) {
120
+ return {
121
+ apiKey: overrides.apiKey ?? args.apiKey,
122
+ apiSecret: overrides.apiSecret ?? args.apiSecret,
123
+ passphrase: overrides.passphrase ?? args.passphrase,
124
+ paper: toBoolean(overrides.paper ?? args.paper, false),
125
+ baseUrl: overrides.baseUrl ?? args.baseUrl,
126
+ wsUrl: overrides.wsUrl ?? args.wsUrl,
127
+ futures: toBoolean(overrides.futures ?? args.futures, false),
128
+ };
129
+ }
130
+
131
+ function createEmaCrossSignal({ fast = 10, slow = 30, rr = 2, stopLookback = 15 } = {}) {
62
132
  return ({ candles }) => {
63
133
  if (candles.length < Math.max(fast, slow) + 2) return null;
64
134
 
@@ -67,27 +137,17 @@ function createEmaCrossSignal({
67
137
  const slowLine = ema(closes, slow);
68
138
  const last = closes.length - 1;
69
139
 
70
- if (
71
- fastLine[last - 1] <= slowLine[last - 1] &&
72
- fastLine[last] > slowLine[last]
73
- ) {
140
+ if (fastLine[last - 1] <= slowLine[last - 1] && fastLine[last] > slowLine[last]) {
74
141
  const entry = candles[last].close;
75
- const stop = Math.min(
76
- ...candles.slice(-stopLookback).map((bar) => bar.low)
77
- );
142
+ const stop = Math.min(...candles.slice(-stopLookback).map((bar) => bar.low));
78
143
  if (entry > stop) {
79
144
  return { side: "long", entry, stop, rr };
80
145
  }
81
146
  }
82
147
 
83
- if (
84
- fastLine[last - 1] >= slowLine[last - 1] &&
85
- fastLine[last] < slowLine[last]
86
- ) {
148
+ if (fastLine[last - 1] >= slowLine[last - 1] && fastLine[last] < slowLine[last]) {
87
149
  const entry = candles[last].close;
88
- const stop = Math.max(
89
- ...candles.slice(-stopLookback).map((bar) => bar.high)
90
- );
150
+ const stop = Math.max(...candles.slice(-stopLookback).map((bar) => bar.high));
91
151
  if (entry < stop) {
92
152
  return { side: "short", entry, stop, rr };
93
153
  }
@@ -170,9 +230,7 @@ async function loadWalkForwardStrategy(strategyArg, args) {
170
230
  const resolved = path.resolve(process.cwd(), strategyArg);
171
231
  const module = await import(pathToFileURL(resolved).href);
172
232
  if (typeof module.signalFactory !== "function") {
173
- throw new Error(
174
- `Walk-forward strategy module "${strategyArg}" must export signalFactory`
175
- );
233
+ throw new Error(`Walk-forward strategy module "${strategyArg}" must export signalFactory`);
176
234
  }
177
235
 
178
236
  const parameterSets =
@@ -196,12 +254,17 @@ async function loadWalkForwardStrategy(strategyArg, args) {
196
254
  }
197
255
 
198
256
  function printHelp() {
199
- console.log(`tradelab
257
+ console.log(`tradelab — backtesting toolkit for Node.js
258
+
259
+ Usage: tradelab <command> [options]
200
260
 
201
261
  Commands:
202
262
  backtest Run a one-off backtest from Yahoo or CSV data
203
263
  portfolio Run multiple CSV datasets as an equal-weight portfolio
204
264
  walk-forward Run rolling or anchored train/test optimization
265
+ live Run live trading engine (streaming or polling)
266
+ paper Run live engine in paper broker mode
267
+ status Read persisted live state
205
268
  prefetch Download Yahoo candles into the local cache
206
269
  import-csv Normalize a CSV and save it into the local cache
207
270
 
@@ -209,12 +272,21 @@ Examples:
209
272
  tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
210
273
  tradelab backtest --source csv --csvPath ./data/btc.csv --strategy buy-hold --holdBars 3
211
274
  tradelab walk-forward --source csv --csvPath ./data/spy.csv --trainBars 120 --testBars 40
275
+ tradelab live --strategy ./mySignal.js --symbol AAPL --interval 5m --broker alpaca --paper
276
+
277
+ Options:
278
+ --help Show this help message
279
+ --version Print version number
212
280
  `);
213
281
  }
214
282
 
215
283
  async function commandBacktest(args) {
284
+ const source = args.source || (args.csvPath ? "csv" : "yahoo");
285
+ if (source === "yahoo" && !args.symbol) {
286
+ throw new Error("backtest with Yahoo source requires --symbol (e.g. --symbol SPY)");
287
+ }
216
288
  const candles = await getHistoricalCandles({
217
- source: args.source || (args.csvPath ? "csv" : "yahoo"),
289
+ source,
218
290
  symbol: args.symbol,
219
291
  interval: args.interval || "1d",
220
292
  period: args.period || "1y",
@@ -311,8 +383,12 @@ async function commandPortfolio(args) {
311
383
  }
312
384
 
313
385
  async function commandWalkForward(args) {
386
+ const wfSource = args.source || (args.csvPath ? "csv" : "yahoo");
387
+ if (wfSource === "yahoo" && !args.symbol) {
388
+ throw new Error("walk-forward with Yahoo source requires --symbol (e.g. --symbol QQQ)");
389
+ }
314
390
  const candles = await getHistoricalCandles({
315
- source: args.source || (args.csvPath ? "csv" : "yahoo"),
391
+ source: wfSource,
316
392
  symbol: args.symbol,
317
393
  interval: args.interval || "1d",
318
394
  period: args.period || "1y",
@@ -398,10 +474,174 @@ async function commandImportCsv(args) {
398
474
  console.log(`Saved ${candles.length} candles to ${outputPath}`);
399
475
  }
400
476
 
477
+ async function createLiveSystemFromConfig(system, args) {
478
+ const signal = await loadStrategy(system.strategy || args.strategy, {
479
+ ...args,
480
+ ...system,
481
+ });
482
+ return {
483
+ ...system,
484
+ signal,
485
+ interval: system.interval || args.interval || "1m",
486
+ symbol: system.symbol || args.symbol,
487
+ };
488
+ }
489
+
490
+ async function commandLive(args, overrides = {}) {
491
+ const configPath = overrides.config || args.config;
492
+ const mode = overrides.mode || args.mode || "streaming";
493
+ const stateDir = overrides.stateDir || args.stateDir || "output/live-state";
494
+ const once = toBoolean(overrides.once ?? args.once, mode === "polling");
495
+ const watch = toBoolean(overrides.watch ?? args.watch, false);
496
+ const storage = new JsonFileStorage({ baseDir: stateDir });
497
+
498
+ if (configPath) {
499
+ const fileConfig = loadJsonFile(configPath);
500
+ const broker = createBrokerAdapter(args, {
501
+ ...overrides,
502
+ equity: fileConfig.equity ?? overrides.equity,
503
+ });
504
+ const brokerConfig = brokerConfigFromArgs(args, overrides);
505
+ const systems = await Promise.all(
506
+ (fileConfig.systems || []).map((system) => createLiveSystemFromConfig(system, args))
507
+ );
508
+ const orchestrator = new LiveOrchestrator({
509
+ systems,
510
+ broker,
511
+ storage,
512
+ brokerConfig,
513
+ allocation: fileConfig.allocation || args.allocation || "equal",
514
+ maxDailyLossPct: toNumber(fileConfig.maxDailyLossPct ?? args.maxDailyLossPct, 0),
515
+ equity: toNumber(fileConfig.equity ?? args.equity, 10_000),
516
+ });
517
+ await broker.connect(brokerConfig);
518
+ await orchestrator.start();
519
+
520
+ if (once && orchestrator.engines?.length) {
521
+ await Promise.all(orchestrator.engines.map((engine) => engine.pollOnce()));
522
+ }
523
+
524
+ const status = orchestrator.getStatus();
525
+ console.log(JSON.stringify(status, null, 2));
526
+
527
+ if (!watch) {
528
+ await orchestrator.stop();
529
+ }
530
+ return;
531
+ }
532
+
533
+ const broker = createBrokerAdapter(args, overrides);
534
+ const brokerConfig = brokerConfigFromArgs(args, overrides);
535
+ const signal = await loadStrategy(overrides.strategy || args.strategy, args);
536
+ const engine = new LiveEngine({
537
+ id: overrides.id || args.id,
538
+ signal,
539
+ symbol: overrides.symbol || args.symbol,
540
+ interval: overrides.interval || args.interval || "1m",
541
+ mode,
542
+ pollIntervalMs: toNumber(overrides.pollIntervalMs ?? args.pollIntervalMs, 60_000),
543
+ warmupBars: toNumber(overrides.warmupBars ?? args.warmupBars, 200),
544
+ equity: toNumber(overrides.equity ?? args.equity, 10_000),
545
+ riskPct: toNumber(overrides.riskPct ?? args.riskPct, 1),
546
+ costs: parseJsonValue(overrides.costs ?? args.costs, null),
547
+ flattenAtClose: toBoolean(overrides.flattenAtClose ?? args.flattenAtClose, false),
548
+ maxDailyLossPct: toNumber(overrides.maxDailyLossPct ?? args.maxDailyLossPct, 0),
549
+ dailyMaxTrades: toNumber(overrides.dailyMaxTrades ?? args.dailyMaxTrades, 0),
550
+ broker,
551
+ storage,
552
+ brokerConfig,
553
+ });
554
+
555
+ await engine.start();
556
+
557
+ if (once) {
558
+ await engine.pollOnce();
559
+ }
560
+
561
+ const status = engine.getStatus();
562
+ console.log(JSON.stringify(status, null, 2));
563
+
564
+ if (!watch) {
565
+ await engine.stop();
566
+ return;
567
+ }
568
+
569
+ const shutdown = async () => {
570
+ await engine.stop();
571
+ process.exit(0);
572
+ };
573
+ process.once("SIGINT", shutdown);
574
+ process.once("SIGTERM", shutdown);
575
+ }
576
+
577
+ async function commandPaper(args) {
578
+ return commandLive(args, { paper: true, broker: "paper" });
579
+ }
580
+
581
+ async function commandStatus(args) {
582
+ const stateDir = args.dir || args.stateDir || "output/live-state";
583
+ const storage = new JsonFileStorage({ baseDir: stateDir });
584
+ const namespace = args.namespace || args.id;
585
+
586
+ if (namespace) {
587
+ const state = await storage.load(namespace);
588
+ const trades = await storage.loadTrades(namespace);
589
+ const equity = await storage.loadEquityCurve(namespace);
590
+ console.log(
591
+ JSON.stringify(
592
+ {
593
+ namespace,
594
+ state,
595
+ trades: trades.length,
596
+ equityPoints: equity.length,
597
+ },
598
+ null,
599
+ 2
600
+ )
601
+ );
602
+ return;
603
+ }
604
+
605
+ if (!fs.existsSync(stateDir)) {
606
+ console.log(JSON.stringify({ dir: stateDir, namespaces: [] }, null, 2));
607
+ return;
608
+ }
609
+
610
+ const namespaces = fs
611
+ .readdirSync(stateDir, { withFileTypes: true })
612
+ .filter((entry) => entry.isDirectory())
613
+ .map((entry) => entry.name);
614
+ const summaries = [];
615
+ for (const name of namespaces) {
616
+ const state = await storage.load(name);
617
+ const trades = await storage.loadTrades(name);
618
+ summaries.push({
619
+ namespace: name,
620
+ savedAt: state?.savedAt ?? null,
621
+ equity: state?.equity ?? null,
622
+ openPosition: Boolean(state?.openPosition),
623
+ trades: trades.length,
624
+ });
625
+ }
626
+ console.log(
627
+ JSON.stringify(
628
+ {
629
+ dir: stateDir,
630
+ namespaces: summaries,
631
+ },
632
+ null,
633
+ 2
634
+ )
635
+ );
636
+ }
637
+
401
638
  const commands = {
402
639
  backtest: commandBacktest,
403
640
  portfolio: commandPortfolio,
404
641
  "walk-forward": commandWalkForward,
642
+ live: commandLive,
643
+ paper: commandPaper,
644
+ status: commandStatus,
405
645
  prefetch: commandPrefetch,
406
646
  "import-csv": commandImportCsv,
407
647
  };
@@ -410,6 +650,12 @@ async function main() {
410
650
  const args = parseArgs(process.argv.slice(2));
411
651
  const command = args._[0];
412
652
 
653
+ if (args.version || args.v) {
654
+ const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
655
+ console.log(pkg.version);
656
+ return;
657
+ }
658
+
413
659
  if (!command || command === "help" || args.help) {
414
660
  printHelp();
415
661
  return;