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.
- package/README.md +89 -41
- package/bin/tradelab.js +276 -30
- package/dist/cjs/data.cjs +134 -104
- package/dist/cjs/index.cjs +378 -177
- package/dist/cjs/live.cjs +3350 -0
- package/docs/README.md +21 -9
- package/docs/api-reference.md +87 -29
- package/docs/backtest-engine.md +37 -53
- package/docs/data-reporting-cli.md +60 -34
- package/docs/examples.md +6 -12
- package/docs/live-trading.md +186 -0
- package/examples/yahooEmaCross.js +1 -6
- package/package.json +18 -3
- package/src/data/csv.js +24 -14
- package/src/data/index.js +1 -5
- package/src/data/yahoo.js +6 -19
- package/src/engine/backtest.js +137 -144
- package/src/engine/backtestTicks.js +89 -37
- package/src/engine/barSystemRunner.js +182 -118
- package/src/engine/execution.js +11 -39
- package/src/engine/portfolio.js +54 -6
- package/src/engine/walkForward.js +37 -14
- package/src/index.js +2 -11
- package/src/live/broker/alpaca.js +254 -0
- package/src/live/broker/binance.js +351 -0
- package/src/live/broker/coinbase.js +339 -0
- package/src/live/broker/interactiveBrokers.js +123 -0
- package/src/live/broker/interface.js +74 -0
- package/src/live/clock.js +56 -0
- package/src/live/engine/candleAggregator.js +154 -0
- package/src/live/engine/liveEngine.js +694 -0
- package/src/live/engine/paperEngine.js +453 -0
- package/src/live/engine/riskManager.js +185 -0
- package/src/live/engine/stateManager.js +112 -0
- package/src/live/events.js +48 -0
- package/src/live/feed/brokerFeed.js +35 -0
- package/src/live/feed/interface.js +28 -0
- package/src/live/feed/pollingFeed.js +105 -0
- package/src/live/index.js +27 -0
- package/src/live/logger.js +82 -0
- package/src/live/orchestrator.js +133 -0
- package/src/live/storage/interface.js +36 -0
- package/src/live/storage/jsonFileStorage.js +112 -0
- package/src/metrics/buildMetrics.js +18 -41
- package/src/reporting/exportBacktestArtifacts.js +1 -4
- package/src/reporting/exportTradesCsv.js +2 -7
- package/src/reporting/renderHtmlReport.js +8 -13
- package/src/utils/indicators.js +1 -2
- package/src/utils/positionSizing.js +16 -2
- package/src/utils/time.js +4 -12
- package/templates/report.html +23 -9
- package/templates/report.js +83 -69
- package/types/index.d.ts +21 -3
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
[](https://www.npmjs.com/package/tradelab)
|
|
7
|
+
[](https://github.com/ishsharm0/tradelab)
|
|
8
|
+
[](https://github.com/ishsharm0/tradelab/blob/main/LICENSE)
|
|
9
|
+
[](https://nodejs.org)
|
|
10
|
+
[](https://github.com/ishsharm0/tradelab/blob/main/types/index.d.ts)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
**tradelab** handles
|
|
16
|
+
**tradelab** handles strategy research and execution workflows in one package.
|
|
17
17
|
|
|
18
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
| **Engine**
|
|
48
|
-
| **Portfolio**
|
|
49
|
-
| **Walk-forward**
|
|
50
|
-
| **
|
|
51
|
-
| **
|
|
52
|
-
| **
|
|
53
|
-
| **
|
|
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,
|
|
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
|
|
165
|
-
|
|
166
|
-
| `equity`
|
|
167
|
-
| `riskPct`
|
|
168
|
-
| `warmupBars`
|
|
169
|
-
| `flattenAtClose`
|
|
170
|
-
| `costs`
|
|
171
|
-
| `strict`
|
|
172
|
-
| `collectEqSeries` | Enables equity curve output
|
|
173
|
-
| `collectReplay`
|
|
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,
|
|
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
|
|
306
|
-
|
|
307
|
-
| `exportHtmlReport(options)`
|
|
308
|
-
| `renderHtmlReport(options)`
|
|
309
|
-
| `exportTradesCsv(trades, options)` | Flat trade ledger for spreadsheets or pandas
|
|
310
|
-
| `exportMetricsJSON(options)`
|
|
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
|
|
381
|
-
|
|
382
|
-
| [Backtest engine](docs/backtest-engine.md)
|
|
383
|
-
| [Data, reporting, and CLI](docs/data-reporting-cli.md) | Data loading, cache behavior, exports, CLI reference
|
|
384
|
-
| [
|
|
385
|
-
| [
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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:
|
|
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;
|