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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Live trading
|
|
2
|
+
|
|
3
|
+
<small>[Back to main page](README.md)</small>
|
|
4
|
+
|
|
5
|
+
This guide covers the `tradelab/live` module and the live CLI commands.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
The live stack is built to reuse the same signal contract as backtesting:
|
|
10
|
+
|
|
11
|
+
- write and validate `signal()` with `backtest()`
|
|
12
|
+
- run the same signal in `LiveEngine` or `LiveOrchestrator`
|
|
13
|
+
- choose a real broker adapter or `PaperEngine`
|
|
14
|
+
- persist state with `JsonFileStorage` for restart safety
|
|
15
|
+
|
|
16
|
+
Import path:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { LiveEngine, LiveOrchestrator, PaperEngine } from "tradelab/live";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Module components
|
|
23
|
+
|
|
24
|
+
| Component | Purpose |
|
|
25
|
+
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
26
|
+
| `LiveEngine` | Single-system live or paper execution loop |
|
|
27
|
+
| `LiveOrchestrator` | Multi-system live execution with shared broker and aggregated status |
|
|
28
|
+
| `PaperEngine` | In-process broker simulator implementing the broker adapter contract |
|
|
29
|
+
| `AlpacaBroker` / `BinanceBroker` / `CoinbaseBroker` / `InteractiveBrokersBroker` | Real broker adapters |
|
|
30
|
+
| `BrokerFeed` / `PollingFeed` | Feed adapters for streaming or polling operation |
|
|
31
|
+
| `RiskManager` | Session windows, daily loss gates, drawdown halts, position checks |
|
|
32
|
+
| `StateManager` / `JsonFileStorage` | Persisted state, trades, and equity curve |
|
|
33
|
+
| `EventBus` / `LiveLogger` | Event fanout and structured logging |
|
|
34
|
+
|
|
35
|
+
## `LiveEngine` quick start
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { LiveEngine, PaperEngine, JsonFileStorage } from "tradelab/live";
|
|
39
|
+
|
|
40
|
+
const engine = new LiveEngine({
|
|
41
|
+
id: "aapl-1m",
|
|
42
|
+
symbol: "AAPL",
|
|
43
|
+
interval: "1m",
|
|
44
|
+
broker: new PaperEngine({ equity: 25_000 }),
|
|
45
|
+
storage: new JsonFileStorage({ baseDir: "./output/live-state" }),
|
|
46
|
+
riskPct: 1,
|
|
47
|
+
mode: "streaming",
|
|
48
|
+
signal({ bar, openPosition }) {
|
|
49
|
+
if (openPosition) return null;
|
|
50
|
+
return {
|
|
51
|
+
side: "long",
|
|
52
|
+
stop: bar.close - 1,
|
|
53
|
+
rr: 2,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await engine.start();
|
|
59
|
+
// ... run until shutdown condition
|
|
60
|
+
await engine.stop();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Important behavior:
|
|
64
|
+
|
|
65
|
+
- `signal()` is called with the same context shape as backtesting
|
|
66
|
+
- market and limit/stop order lifecycles are tracked through broker events
|
|
67
|
+
- state is persisted after fills, order updates, and equity updates
|
|
68
|
+
- `getStatus()` returns runtime and risk state for health checks
|
|
69
|
+
|
|
70
|
+
## `LiveOrchestrator` quick start
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { LiveOrchestrator, PaperEngine, JsonFileStorage } from "tradelab/live";
|
|
74
|
+
|
|
75
|
+
const orchestrator = new LiveOrchestrator({
|
|
76
|
+
broker: new PaperEngine({ equity: 100_000 }),
|
|
77
|
+
storage: new JsonFileStorage({ baseDir: "./output/live-state" }),
|
|
78
|
+
allocation: "weight",
|
|
79
|
+
systems: [
|
|
80
|
+
{ id: "spy", symbol: "SPY", interval: "1m", weight: 2, signal: signalA },
|
|
81
|
+
{ id: "qqq", symbol: "QQQ", interval: "1m", weight: 1, signal: signalB },
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await orchestrator.start();
|
|
86
|
+
const status = orchestrator.getStatus();
|
|
87
|
+
await orchestrator.stop();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use orchestrator when multiple systems should share one broker/account context.
|
|
91
|
+
|
|
92
|
+
## CLI live commands
|
|
93
|
+
|
|
94
|
+
| Command | Purpose |
|
|
95
|
+
| ----------------- | -------------------------------------------- |
|
|
96
|
+
| `tradelab live` | Run live engine or orchestrator (`--config`) |
|
|
97
|
+
| `tradelab paper` | Shortcut for `live` with paper broker mode |
|
|
98
|
+
| `tradelab status` | Inspect persisted live state |
|
|
99
|
+
|
|
100
|
+
### Single-system paper run
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
tradelab paper \
|
|
104
|
+
--id aapl-1m \
|
|
105
|
+
--symbol AAPL \
|
|
106
|
+
--interval 1m \
|
|
107
|
+
--mode polling \
|
|
108
|
+
--once true \
|
|
109
|
+
--stateDir ./output/live-state
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Orchestrator run from config
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
tradelab live \
|
|
116
|
+
--config ./live-portfolio.json \
|
|
117
|
+
--paper \
|
|
118
|
+
--mode polling \
|
|
119
|
+
--once true \
|
|
120
|
+
--stateDir ./output/live-state
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Example config:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"allocation": "weight",
|
|
128
|
+
"equity": 50000,
|
|
129
|
+
"systems": [
|
|
130
|
+
{
|
|
131
|
+
"id": "spy-system",
|
|
132
|
+
"symbol": "SPY",
|
|
133
|
+
"interval": "1m",
|
|
134
|
+
"strategy": "./strategies/spySignal.js",
|
|
135
|
+
"weight": 2
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"id": "qqq-system",
|
|
139
|
+
"symbol": "QQQ",
|
|
140
|
+
"interval": "1m",
|
|
141
|
+
"strategy": "./strategies/qqqSignal.js",
|
|
142
|
+
"weight": 1
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### State inspection
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
tradelab status --dir ./output/live-state
|
|
152
|
+
tradelab status --dir ./output/live-state --namespace spy-system
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## State and recovery
|
|
156
|
+
|
|
157
|
+
Live state is namespaced and persisted as:
|
|
158
|
+
|
|
159
|
+
- `state.json` (latest engine state)
|
|
160
|
+
- `trades.jsonl` (append-only)
|
|
161
|
+
- `equity.jsonl` (append-only)
|
|
162
|
+
|
|
163
|
+
On restart, the engine loads persisted state and reconciles with broker positions.
|
|
164
|
+
|
|
165
|
+
## Broker notes
|
|
166
|
+
|
|
167
|
+
- Alpaca and Binance adapters support native paper modes.
|
|
168
|
+
- Coinbase adapter is live API only; use `PaperEngine` for simulated Coinbase workflows.
|
|
169
|
+
- Interactive Brokers adapter requires `@stoqey/ib` to be installed.
|
|
170
|
+
|
|
171
|
+
For runtime compatibility and options, see [types/live.d.ts](../types/live.d.ts).
|
|
172
|
+
|
|
173
|
+
## Eventing and logs
|
|
174
|
+
|
|
175
|
+
`EventBus` emits lifecycle and execution events such as:
|
|
176
|
+
|
|
177
|
+
- `connected`, `shutdown`
|
|
178
|
+
- `signal`
|
|
179
|
+
- `order:submitted`, `order:filled`, `order:rejected`, `order:canceled`
|
|
180
|
+
- `position:opened`, `position:closed`
|
|
181
|
+
- `equity:update`
|
|
182
|
+
- `risk:warning`, `risk:halt`
|
|
183
|
+
|
|
184
|
+
Attach `LiveLogger` for structured JSON logs.
|
|
185
|
+
|
|
186
|
+
<small>[Back to main page](README.md)</small>
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
backtest,
|
|
6
|
-
ema,
|
|
7
|
-
exportBacktestArtifacts,
|
|
8
|
-
getHistoricalCandles,
|
|
9
|
-
} from "../src/index.js";
|
|
4
|
+
import { backtest, ema, exportBacktestArtifacts, getHistoricalCandles } from "../src/index.js";
|
|
10
5
|
|
|
11
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
7
|
const __dirname = path.dirname(__filename);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tradelab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Backtesting toolkit for Node.js with strategy simulation, historical data loading, and report generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.cjs",
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"import": "./src/data/index.js",
|
|
34
34
|
"require": "./dist/cjs/data.cjs"
|
|
35
35
|
},
|
|
36
|
+
"./live": {
|
|
37
|
+
"types": "./types/live.d.ts",
|
|
38
|
+
"import": "./src/live/index.js",
|
|
39
|
+
"require": "./dist/cjs/live.cjs"
|
|
40
|
+
},
|
|
36
41
|
"./package.json": "./package.json"
|
|
37
42
|
},
|
|
38
43
|
"files": [
|
|
@@ -48,8 +53,12 @@
|
|
|
48
53
|
],
|
|
49
54
|
"scripts": {
|
|
50
55
|
"build": "node scripts/build-cjs.mjs",
|
|
56
|
+
"lint": "eslint .",
|
|
57
|
+
"lint:fix": "eslint . --fix",
|
|
58
|
+
"format": "prettier . --write",
|
|
59
|
+
"format:check": "prettier . --check",
|
|
60
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
51
61
|
"prepare": "npm run build",
|
|
52
|
-
"prepack": "npm run build",
|
|
53
62
|
"test": "node --test"
|
|
54
63
|
},
|
|
55
64
|
"keywords": [
|
|
@@ -67,6 +76,12 @@
|
|
|
67
76
|
"access": "public"
|
|
68
77
|
},
|
|
69
78
|
"devDependencies": {
|
|
70
|
-
"
|
|
79
|
+
"@eslint/js": "^9.25.1",
|
|
80
|
+
"@types/node": "^22.15.2",
|
|
81
|
+
"esbuild": "^0.27.3",
|
|
82
|
+
"eslint": "^9.25.1",
|
|
83
|
+
"globals": "^15.15.0",
|
|
84
|
+
"prettier": "^3.5.3",
|
|
85
|
+
"typescript": "^5.8.3"
|
|
71
86
|
}
|
|
72
87
|
}
|
package/src/data/csv.js
CHANGED
|
@@ -21,7 +21,9 @@ function resolveDate(value, customDateParser) {
|
|
|
21
21
|
if (Number.isFinite(time)) return time;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const raw = String(value)
|
|
24
|
+
const raw = String(value)
|
|
25
|
+
.trim()
|
|
26
|
+
.replace(/^['"]|['"]$/g, "");
|
|
25
27
|
const numeric = Number(raw);
|
|
26
28
|
if (Number.isFinite(numeric)) {
|
|
27
29
|
return numeric < 1e11 ? numeric * 1000 : numeric;
|
|
@@ -108,7 +110,7 @@ function normalizeDateBoundary(value, fallback) {
|
|
|
108
110
|
export function normalizeCandles(candles) {
|
|
109
111
|
if (!Array.isArray(candles)) return [];
|
|
110
112
|
|
|
111
|
-
const
|
|
113
|
+
const parsed = candles
|
|
112
114
|
.map((bar) => {
|
|
113
115
|
try {
|
|
114
116
|
const time = resolveDate(bar?.time ?? bar?.timestamp ?? bar?.date);
|
|
@@ -140,8 +142,18 @@ export function normalizeCandles(candles) {
|
|
|
140
142
|
return null;
|
|
141
143
|
}
|
|
142
144
|
})
|
|
143
|
-
.filter(Boolean)
|
|
144
|
-
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
|
|
147
|
+
let reordered = false;
|
|
148
|
+
let duplicateCount = 0;
|
|
149
|
+
for (let index = 1; index < parsed.length; index += 1) {
|
|
150
|
+
const prev = parsed[index - 1].time;
|
|
151
|
+
const current = parsed[index].time;
|
|
152
|
+
if (current < prev) reordered = true;
|
|
153
|
+
if (current === prev) duplicateCount += 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const normalized = parsed.sort((left, right) => left.time - right.time);
|
|
145
157
|
|
|
146
158
|
const deduped = [];
|
|
147
159
|
let lastTime = null;
|
|
@@ -150,6 +162,12 @@ export function normalizeCandles(candles) {
|
|
|
150
162
|
deduped.push(candle);
|
|
151
163
|
lastTime = candle.time;
|
|
152
164
|
}
|
|
165
|
+
const removedDuplicates = normalized.length - deduped.length;
|
|
166
|
+
if (reordered || removedDuplicates > 0 || duplicateCount > 0) {
|
|
167
|
+
console.warn(
|
|
168
|
+
`[tradelab] normalizeCandles() reordered or deduplicated candles (input=${candles.length}, valid=${parsed.length}, output=${deduped.length})`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
153
171
|
return deduped;
|
|
154
172
|
}
|
|
155
173
|
|
|
@@ -197,16 +215,8 @@ export function loadCandlesFromCSV(filePath, options = {}) {
|
|
|
197
215
|
const closeIdx = resolveColumn(closeCol, headerIndex, ["c", "adj close"]);
|
|
198
216
|
const volumeIdx = resolveColumn(volumeCol, headerIndex, ["v", "vol", "quantity"]);
|
|
199
217
|
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
openIdx < 0 ||
|
|
203
|
-
highIdx < 0 ||
|
|
204
|
-
lowIdx < 0 ||
|
|
205
|
-
closeIdx < 0
|
|
206
|
-
) {
|
|
207
|
-
throw new Error(
|
|
208
|
-
`Could not resolve required CSV columns in ${path.basename(filePath)}`
|
|
209
|
-
);
|
|
218
|
+
if (timeIdx < 0 || openIdx < 0 || highIdx < 0 || lowIdx < 0 || closeIdx < 0) {
|
|
219
|
+
throw new Error(`Could not resolve required CSV columns in ${path.basename(filePath)}`);
|
|
210
220
|
}
|
|
211
221
|
|
|
212
222
|
const minTime = normalizeDateBoundary(startDate, -Infinity);
|
package/src/data/index.js
CHANGED
|
@@ -97,11 +97,7 @@ export async function getHistoricalCandles(options = {}) {
|
|
|
97
97
|
return candles;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
export async function backtestHistorical({
|
|
101
|
-
backtestOptions = {},
|
|
102
|
-
data,
|
|
103
|
-
...legacy
|
|
104
|
-
} = {}) {
|
|
100
|
+
export async function backtestHistorical({ backtestOptions = {}, data, ...legacy } = {}) {
|
|
105
101
|
const candles = await getHistoricalCandles(data || legacy);
|
|
106
102
|
return runBacktest({
|
|
107
103
|
candles,
|
package/src/data/yahoo.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2
2
|
|
|
3
3
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
4
|
-
const DAY_SEC = 24 * 60 * 60;
|
|
5
4
|
const requestQueue = {
|
|
6
5
|
lastRequestAt: 0,
|
|
7
6
|
minDelayMs: 400,
|
|
@@ -111,8 +110,7 @@ async function rateLimitedFetch(url, options = {}) {
|
|
|
111
110
|
return fetch(url, {
|
|
112
111
|
...options,
|
|
113
112
|
headers: {
|
|
114
|
-
"User-Agent":
|
|
115
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
113
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
116
114
|
...options.headers,
|
|
117
115
|
},
|
|
118
116
|
});
|
|
@@ -152,12 +150,7 @@ async function fetchYahooChart(symbol, { period1, period2, interval, includePreP
|
|
|
152
150
|
|
|
153
151
|
const candles = [];
|
|
154
152
|
for (let index = 0; index < timestamps.length; index += 1) {
|
|
155
|
-
if (
|
|
156
|
-
open[index] == null ||
|
|
157
|
-
high[index] == null ||
|
|
158
|
-
low[index] == null ||
|
|
159
|
-
close[index] == null
|
|
160
|
-
) {
|
|
153
|
+
if (open[index] == null || high[index] == null || low[index] == null || close[index] == null) {
|
|
161
154
|
continue;
|
|
162
155
|
}
|
|
163
156
|
|
|
@@ -179,7 +172,7 @@ function formatYahooFailureMessage(symbol, interval, period, error, attempts) {
|
|
|
179
172
|
return [
|
|
180
173
|
`Unable to reach Yahoo Finance for ${symbol} ${interval} ${period} after ${attempts} attempts.`,
|
|
181
174
|
`Last error: ${detail}`,
|
|
182
|
-
|
|
175
|
+
'Try again later, or fall back to a local CSV/cache workflow with getHistoricalCandles({ source: "csv", ... }) or loadCandlesFromCache(...).',
|
|
183
176
|
].join(" ");
|
|
184
177
|
}
|
|
185
178
|
|
|
@@ -203,13 +196,7 @@ async function fetchYahooChartWithRetry(symbol, params, period, maxRetries = 3)
|
|
|
203
196
|
}
|
|
204
197
|
|
|
205
198
|
throw new Error(
|
|
206
|
-
formatYahooFailureMessage(
|
|
207
|
-
symbol,
|
|
208
|
-
params.interval,
|
|
209
|
-
period,
|
|
210
|
-
lastError,
|
|
211
|
-
maxRetries
|
|
212
|
-
)
|
|
199
|
+
formatYahooFailureMessage(symbol, params.interval, period, lastError, maxRetries)
|
|
213
200
|
);
|
|
214
201
|
}
|
|
215
202
|
|
|
@@ -253,10 +240,10 @@ export async function fetchHistorical(symbol, interval = "5m", period = "60d", o
|
|
|
253
240
|
period
|
|
254
241
|
);
|
|
255
242
|
chunks.push(...candles);
|
|
256
|
-
chunkEndMs = chunkStartMs - 1000;
|
|
257
243
|
remainingMs -= takeMs;
|
|
244
|
+
chunkEndMs = chunkStartMs - 1000;
|
|
258
245
|
|
|
259
|
-
if (chunks.length > 2_000_000) break;
|
|
246
|
+
if (chunkEndMs <= 0 || chunks.length > 2_000_000) break;
|
|
260
247
|
}
|
|
261
248
|
|
|
262
249
|
return sanitizeBars(chunks);
|