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 +290 -130
- package/bin/tradelab.js +68 -23
- package/dist/cjs/data.cjs +78 -53
- package/dist/cjs/index.cjs +1518 -211
- package/docs/README.md +66 -0
- package/docs/api-reference.md +75 -0
- package/docs/backtest-engine.md +393 -0
- package/docs/data-reporting-cli.md +258 -0
- package/docs/examples.md +281 -0
- package/package.json +2 -1
- package/src/engine/backtestTicks.js +429 -0
- package/src/engine/barSystemRunner.js +963 -0
- package/src/engine/portfolio.js +191 -68
- package/src/engine/walkForward.js +106 -10
- package/src/index.js +1 -0
- package/src/metrics/buildMetrics.js +89 -63
- package/types/index.d.ts +77 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Data, reporting, and CLI
|
|
2
|
+
<small>[Back to main page](README.md)</small>
|
|
3
|
+
|
|
4
|
+
This page covers the parts of the package around the core engine:
|
|
5
|
+
|
|
6
|
+
- historical data loading
|
|
7
|
+
- local cache helpers
|
|
8
|
+
- export helpers
|
|
9
|
+
- command-line usage
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
If you are not bringing your own candles yet, start here.
|
|
14
|
+
|
|
15
|
+
## Choose the right entry point
|
|
16
|
+
|
|
17
|
+
| Use case | Function |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| Load data without caring about the source-specific helper | `getHistoricalCandles()` |
|
|
20
|
+
| Fetch directly from Yahoo | `fetchHistorical()` |
|
|
21
|
+
| Load a local CSV file | `loadCandlesFromCSV()` |
|
|
22
|
+
| Reuse saved normalized data | `loadCandlesFromCache()` |
|
|
23
|
+
| Try the package from a terminal first | `tradelab` CLI |
|
|
24
|
+
|
|
25
|
+
## Historical data
|
|
26
|
+
|
|
27
|
+
### `getHistoricalCandles(options)`
|
|
28
|
+
|
|
29
|
+
This is the main data-loading entry point.
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
const candles = await getHistoricalCandles({
|
|
33
|
+
source: "yahoo",
|
|
34
|
+
symbol: "SPY",
|
|
35
|
+
interval: "1d",
|
|
36
|
+
period: "2y",
|
|
37
|
+
cache: true,
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Sources
|
|
42
|
+
|
|
43
|
+
- `yahoo`
|
|
44
|
+
- `csv`
|
|
45
|
+
- `auto`
|
|
46
|
+
|
|
47
|
+
`auto` switches to CSV when `csvPath` or `csv.filePath` is present. Otherwise it uses Yahoo.
|
|
48
|
+
|
|
49
|
+
If you are writing application code, prefer `getHistoricalCandles()` over calling source-specific helpers directly.
|
|
50
|
+
|
|
51
|
+
### Yahoo options
|
|
52
|
+
|
|
53
|
+
| Option | Purpose |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `symbol` | Ticker or Yahoo symbol |
|
|
56
|
+
| `interval` | Candle interval such as `1d` or `5m` |
|
|
57
|
+
| `period` | Lookback period such as `6mo` or `1y` |
|
|
58
|
+
| `includePrePost` | Includes premarket and postmarket data when supported |
|
|
59
|
+
| `cache` | Reuses saved normalized data |
|
|
60
|
+
| `refresh` | Forces a fresh download even if cache exists |
|
|
61
|
+
| `cacheDir` | Overrides the default cache directory |
|
|
62
|
+
|
|
63
|
+
The Yahoo layer retries transient failures with exponential backoff. If the endpoint still fails, the error message points users toward CSV or cached data.
|
|
64
|
+
|
|
65
|
+
Use caching for repeatable research runs. It reduces network noise and makes failures easier to diagnose.
|
|
66
|
+
|
|
67
|
+
### CSV options
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
const candles = await getHistoricalCandles({
|
|
71
|
+
source: "csv",
|
|
72
|
+
csvPath: "./data/spy.csv",
|
|
73
|
+
csv: {
|
|
74
|
+
timeCol: "timestamp",
|
|
75
|
+
openCol: "open",
|
|
76
|
+
highCol: "high",
|
|
77
|
+
lowCol: "low",
|
|
78
|
+
closeCol: "close",
|
|
79
|
+
volumeCol: "volume",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
CSV parsing can be configured with:
|
|
85
|
+
|
|
86
|
+
- delimiter
|
|
87
|
+
- header presence
|
|
88
|
+
- column names or indexes
|
|
89
|
+
- start/end date filters
|
|
90
|
+
- custom date parsing
|
|
91
|
+
|
|
92
|
+
If your CSV already uses common OHLCV column names, you often do not need to pass any mapping at all.
|
|
93
|
+
|
|
94
|
+
## Cache helpers
|
|
95
|
+
|
|
96
|
+
Available helpers:
|
|
97
|
+
|
|
98
|
+
- `saveCandlesToCache(candles, meta)`
|
|
99
|
+
- `loadCandlesFromCache(symbol, interval, period, outDir)`
|
|
100
|
+
- `cachedCandlesPath(symbol, interval, period, outDir)`
|
|
101
|
+
|
|
102
|
+
The cache is just normalized candle JSON on disk. It is meant for research convenience, not as a durable database layer.
|
|
103
|
+
|
|
104
|
+
## Common workflows
|
|
105
|
+
|
|
106
|
+
### Yahoo to backtest
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const candles = await getHistoricalCandles({
|
|
110
|
+
source: "yahoo",
|
|
111
|
+
symbol: "SPY",
|
|
112
|
+
interval: "1d",
|
|
113
|
+
period: "1y",
|
|
114
|
+
cache: true,
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### CSV to backtest
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
const candles = await getHistoricalCandles({
|
|
122
|
+
source: "csv",
|
|
123
|
+
csvPath: "./data/spy.csv",
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Cached repeat run
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
const candles = await getHistoricalCandles({
|
|
131
|
+
source: "yahoo",
|
|
132
|
+
symbol: "SPY",
|
|
133
|
+
interval: "1d",
|
|
134
|
+
period: "1y",
|
|
135
|
+
cache: true,
|
|
136
|
+
refresh: false,
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Reporting and exports
|
|
141
|
+
|
|
142
|
+
### `exportBacktestArtifacts({ result, outDir })`
|
|
143
|
+
|
|
144
|
+
The main bundle export. By default it writes:
|
|
145
|
+
|
|
146
|
+
- HTML report
|
|
147
|
+
- trade CSV
|
|
148
|
+
- metrics JSON
|
|
149
|
+
|
|
150
|
+
Return value:
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
{
|
|
154
|
+
csv,
|
|
155
|
+
html,
|
|
156
|
+
metrics
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
If you only need one output type, call the narrower helper directly.
|
|
161
|
+
|
|
162
|
+
### `exportMetricsJSON({ result, outDir })`
|
|
163
|
+
|
|
164
|
+
Use this for dashboards, notebooks, or any machine-readable downstream pipeline.
|
|
165
|
+
|
|
166
|
+
For automation, this is usually the best export format to build on.
|
|
167
|
+
|
|
168
|
+
### `exportTradesCsv(trades, options)`
|
|
169
|
+
|
|
170
|
+
Use this when you want a flat trade ledger for spreadsheets or pandas-style workflows.
|
|
171
|
+
|
|
172
|
+
### `renderHtmlReport(options)` and `exportHtmlReport(options)`
|
|
173
|
+
|
|
174
|
+
- `renderHtmlReport()` returns an HTML string
|
|
175
|
+
- `exportHtmlReport()` writes the file and returns its path
|
|
176
|
+
|
|
177
|
+
The report system uses the assets under `templates/`. The renderer injects the payload and keeps markup, CSS, and client script separate from the JS entrypoint.
|
|
178
|
+
|
|
179
|
+
## CLI
|
|
180
|
+
|
|
181
|
+
The package ships with a `tradelab` binary.
|
|
182
|
+
|
|
183
|
+
The CLI is best for quick iteration, smoke tests, and trying the package before building a JS workflow around it.
|
|
184
|
+
|
|
185
|
+
## Commands
|
|
186
|
+
|
|
187
|
+
| Command | Purpose |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `tradelab backtest` | Run a single backtest from Yahoo or CSV |
|
|
190
|
+
| `tradelab portfolio` | Run a simple multi-file portfolio backtest |
|
|
191
|
+
| `tradelab walk-forward` | Run rolling or anchored validation with built-in or local strategy search |
|
|
192
|
+
| `tradelab prefetch` | Download and cache Yahoo data |
|
|
193
|
+
| `tradelab import-csv` | Normalize and cache a CSV file |
|
|
194
|
+
|
|
195
|
+
### Backtest
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
|
|
199
|
+
tradelab backtest --source csv --csvPath ./data/btc.csv --strategy buy-hold --holdBars 3
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Built-in strategies:
|
|
203
|
+
|
|
204
|
+
- `ema-cross`
|
|
205
|
+
- `buy-hold`
|
|
206
|
+
|
|
207
|
+
You can also point `--strategy` at a local module. The module should export one of:
|
|
208
|
+
|
|
209
|
+
- `default(args)`
|
|
210
|
+
- `createSignal(args)`
|
|
211
|
+
- `signal`
|
|
212
|
+
|
|
213
|
+
That makes it easy to prototype a strategy file before wiring it into a larger application.
|
|
214
|
+
|
|
215
|
+
### Portfolio
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
tradelab portfolio \
|
|
219
|
+
--csvPaths ./data/spy.csv,./data/qqq.csv \
|
|
220
|
+
--symbols SPY,QQQ \
|
|
221
|
+
--strategy buy-hold
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
This command is intentionally simple. Use it for quick combined runs, not for custom portfolio logic.
|
|
225
|
+
|
|
226
|
+
### Walk-forward
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
tradelab walk-forward \
|
|
230
|
+
--source yahoo \
|
|
231
|
+
--symbol QQQ \
|
|
232
|
+
--interval 1d \
|
|
233
|
+
--period 2y \
|
|
234
|
+
--trainBars 180 \
|
|
235
|
+
--testBars 60 \
|
|
236
|
+
--mode anchored
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The CLI walk-forward command defaults to the built-in `ema-cross` search, but `--strategy ./path/to/module.mjs` can now load a local module that exports `signalFactory(params, args)` and either `parameterSets` or `createParameterSets(args)`. Inline JSON grids are also accepted through `--parameterSets`.
|
|
240
|
+
|
|
241
|
+
### Cache utilities
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
tradelab prefetch --symbol SPY --interval 1d --period 1y
|
|
245
|
+
tradelab import-csv --csvPath ./data/spy.csv --symbol SPY --interval 1d
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Troubleshooting
|
|
249
|
+
|
|
250
|
+
| Problem | Check first |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| Yahoo request errors | enable cache, retry later, or fall back to CSV |
|
|
253
|
+
| Unexpected trade count | `warmupBars`, `flattenAtClose`, and signal frequency |
|
|
254
|
+
| Empty result | candle order, signal logic, and stop/target validity |
|
|
255
|
+
| Confusing CSV import | inspect normalized bars from `loadCandlesFromCSV()` before backtesting |
|
|
256
|
+
| Export confusion | use metrics JSON first if you need programmatic output |
|
|
257
|
+
|
|
258
|
+
<small>[Back to main page](README.md)</small>
|
package/docs/examples.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Strategy examples
|
|
2
|
+
<small>[Back to main page](README.md)</small>
|
|
3
|
+
|
|
4
|
+
These are research templates. They show how to wire different kinds of data and execution assumptions into the engine without changing the output pipeline.
|
|
5
|
+
|
|
6
|
+
The five examples cover:
|
|
7
|
+
|
|
8
|
+
- single-symbol price research
|
|
9
|
+
- tick-level fills
|
|
10
|
+
- external feature overlays
|
|
11
|
+
- model-derived regime filters with walk-forward validation
|
|
12
|
+
- portfolio research with shared capital
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Mean reversion pullback
|
|
17
|
+
|
|
18
|
+
Entry when price is stretched below its 20-bar mean. Exit via stop and take-profit.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { backtest, getHistoricalCandles } from "tradelab";
|
|
22
|
+
|
|
23
|
+
function sma(values, period) {
|
|
24
|
+
if (values.length < period) return null;
|
|
25
|
+
return values.slice(-period).reduce((sum, v) => sum + v, 0) / period;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const candles = await getHistoricalCandles({
|
|
29
|
+
source: "yahoo",
|
|
30
|
+
symbol: "SPY",
|
|
31
|
+
interval: "1d",
|
|
32
|
+
period: "2y",
|
|
33
|
+
cache: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = backtest({
|
|
37
|
+
candles,
|
|
38
|
+
symbol: "SPY",
|
|
39
|
+
warmupBars: 25,
|
|
40
|
+
signal({ candles: history, bar }) {
|
|
41
|
+
const closes = history.map((c) => c.close);
|
|
42
|
+
const mean = sma(closes, 20);
|
|
43
|
+
if (!mean) return null;
|
|
44
|
+
|
|
45
|
+
const stretch = (bar.close - mean) / mean;
|
|
46
|
+
if (stretch > -0.03) return null;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
side: "long",
|
|
50
|
+
entry: bar.close,
|
|
51
|
+
stop: bar.low * 0.99,
|
|
52
|
+
rr: 1.5,
|
|
53
|
+
_maxBarsInTrade: 5,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 2. Opening-range breakout on ticks
|
|
62
|
+
|
|
63
|
+
Breakout logic where fill order matters. `backtestTicks()` resolves fills at tick resolution instead of bar close. The result shape is identical to `backtest()`.
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
import { backtestTicks } from "tradelab";
|
|
67
|
+
|
|
68
|
+
const result = backtestTicks({
|
|
69
|
+
ticks,
|
|
70
|
+
symbol: "NQ",
|
|
71
|
+
equity: 25_000,
|
|
72
|
+
queueFillProbability: 0.4,
|
|
73
|
+
costs: {
|
|
74
|
+
spreadBps: 0.5,
|
|
75
|
+
slippageByKind: {
|
|
76
|
+
market: 2,
|
|
77
|
+
stop: 4,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
signal({ candles: history, bar, index }) {
|
|
81
|
+
if (index < 30) return null;
|
|
82
|
+
|
|
83
|
+
const openingRange = history.slice(0, 30);
|
|
84
|
+
const rangeHigh = Math.max(...openingRange.map((t) => t.high));
|
|
85
|
+
const rangeLow = Math.min(...openingRange.map((t) => t.low));
|
|
86
|
+
|
|
87
|
+
if (bar.close > rangeHigh) {
|
|
88
|
+
return { side: "long", entry: rangeHigh, stop: rangeLow, rr: 2 };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (bar.close < rangeLow) {
|
|
92
|
+
return { side: "short", entry: rangeLow, stop: rangeHigh, rr: 2 };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`queueFillProbability` controls what fraction of limit touches actually fill. Set it to `1` for optimistic fills, `0` to require the price to trade through.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 3. Sentiment overlay on a candle strategy
|
|
105
|
+
|
|
106
|
+
Enrich candles with a second data source before the backtest starts. The engine does not care where extra fields come from.
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { backtest, getHistoricalCandles, ema } from "tradelab";
|
|
110
|
+
|
|
111
|
+
const candles = await getHistoricalCandles({
|
|
112
|
+
source: "yahoo",
|
|
113
|
+
symbol: "AAPL",
|
|
114
|
+
interval: "1d",
|
|
115
|
+
period: "2y",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const sentimentByDay = new Map([
|
|
119
|
+
["2025-01-02", 0.75],
|
|
120
|
+
["2025-01-03", -0.10],
|
|
121
|
+
// ...
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const enriched = candles.map((bar) => ({
|
|
125
|
+
...bar,
|
|
126
|
+
sentiment:
|
|
127
|
+
sentimentByDay.get(new Date(bar.time).toISOString().slice(0, 10)) ?? 0,
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
const result = backtest({
|
|
131
|
+
candles: enriched,
|
|
132
|
+
symbol: "AAPL",
|
|
133
|
+
warmupBars: 30,
|
|
134
|
+
signal({ candles: history, bar }) {
|
|
135
|
+
const closes = history.map((c) => c.close);
|
|
136
|
+
const fast = ema(closes, 10);
|
|
137
|
+
const slow = ema(closes, 30);
|
|
138
|
+
const last = closes.length - 1;
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
fast[last - 1] <= slow[last - 1] &&
|
|
142
|
+
fast[last] > slow[last] &&
|
|
143
|
+
bar.sentiment > 0.5
|
|
144
|
+
) {
|
|
145
|
+
return {
|
|
146
|
+
side: "long",
|
|
147
|
+
entry: bar.close,
|
|
148
|
+
stop: Math.min(...history.slice(-10).map((c) => c.low)),
|
|
149
|
+
rr: 2,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The same pattern works for any precomputed field - regime labels, macro scores, alternative data signals. Compute it outside the engine, attach it to the candle, read it in the signal function.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 4. Precomputed regime filter with anchored walk-forward
|
|
163
|
+
|
|
164
|
+
LLM or model outputs work best as precomputed fields, not as live callers inside the signal function. Call the model once per bar outside the engine, store the result on the candle, then run a normal walk-forward on top of it.
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
import { walkForwardOptimize, getHistoricalCandles, ema } from "tradelab";
|
|
168
|
+
|
|
169
|
+
const candles = await getHistoricalCandles({
|
|
170
|
+
source: "yahoo",
|
|
171
|
+
symbol: "QQQ",
|
|
172
|
+
interval: "1d",
|
|
173
|
+
period: "3y",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// call model outside the engine - keep signal() synchronous
|
|
177
|
+
const labeled = await Promise.all(
|
|
178
|
+
candles.map(async (bar, index) => ({
|
|
179
|
+
...bar,
|
|
180
|
+
regime:
|
|
181
|
+
index < 20
|
|
182
|
+
? "neutral"
|
|
183
|
+
: await classifyRegime(
|
|
184
|
+
candles.slice(index - 20, index).map((c) => c.close)
|
|
185
|
+
),
|
|
186
|
+
}))
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const wf = walkForwardOptimize({
|
|
190
|
+
candles: labeled,
|
|
191
|
+
mode: "anchored",
|
|
192
|
+
trainBars: 180,
|
|
193
|
+
testBars: 60,
|
|
194
|
+
stepBars: 60,
|
|
195
|
+
scoreBy: "profitFactor",
|
|
196
|
+
parameterSets: [
|
|
197
|
+
{ fast: 10, slow: 30, regime: "trend" },
|
|
198
|
+
{ fast: 20, slow: 50, regime: "trend" },
|
|
199
|
+
{ fast: 10, slow: 30, regime: "mean-revert" },
|
|
200
|
+
],
|
|
201
|
+
backtestOptions: {
|
|
202
|
+
warmupBars: 60,
|
|
203
|
+
flattenAtClose: false,
|
|
204
|
+
},
|
|
205
|
+
signalFactory(params) {
|
|
206
|
+
return ({ candles: history, bar }) => {
|
|
207
|
+
if (bar.regime !== params.regime) return null;
|
|
208
|
+
|
|
209
|
+
const closes = history.map((c) => c.close);
|
|
210
|
+
const fast = ema(closes, params.fast);
|
|
211
|
+
const slow = ema(closes, params.slow);
|
|
212
|
+
const last = closes.length - 1;
|
|
213
|
+
|
|
214
|
+
if (fast[last - 1] <= slow[last - 1] && fast[last] > slow[last]) {
|
|
215
|
+
return {
|
|
216
|
+
side: "long",
|
|
217
|
+
entry: bar.close,
|
|
218
|
+
stop: Math.min(...history.slice(-15).map((c) => c.low)),
|
|
219
|
+
rr: 2,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return null;
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Check `wf.bestParamsSummary` for parameter stability across windows. If the winning regime or EMA pair changes every window, the model output probably is not adding signal.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 5. Cross-sectional momentum portfolio
|
|
234
|
+
|
|
235
|
+
One signal factory across three symbols. Fills compete for the same capital pool at fill time - a position on SPY reduces what QQQ and IWM can size into on the same bar.
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
import { backtestPortfolio, ema, getHistoricalCandles } from "tradelab";
|
|
239
|
+
|
|
240
|
+
function momentumSignal() {
|
|
241
|
+
return ({ candles: history }) => {
|
|
242
|
+
if (history.length < 60) return null;
|
|
243
|
+
|
|
244
|
+
const closes = history.map((c) => c.close);
|
|
245
|
+
const fast = ema(closes, 20);
|
|
246
|
+
const slow = ema(closes, 50);
|
|
247
|
+
const last = closes.length - 1;
|
|
248
|
+
|
|
249
|
+
if (fast[last - 1] <= slow[last - 1] && fast[last] > slow[last]) {
|
|
250
|
+
return {
|
|
251
|
+
side: "long",
|
|
252
|
+
entry: closes[last],
|
|
253
|
+
stop: Math.min(...history.slice(-20).map((c) => c.low)),
|
|
254
|
+
rr: 2,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return null;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const [spy, qqq, iwm] = await Promise.all([
|
|
263
|
+
getHistoricalCandles({ source: "yahoo", symbol: "SPY", interval: "1d", period: "2y" }),
|
|
264
|
+
getHistoricalCandles({ source: "yahoo", symbol: "QQQ", interval: "1d", period: "2y" }),
|
|
265
|
+
getHistoricalCandles({ source: "yahoo", symbol: "IWM", interval: "1d", period: "2y" }),
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
const result = backtestPortfolio({
|
|
269
|
+
equity: 100_000,
|
|
270
|
+
maxDailyLossPct: 3,
|
|
271
|
+
systems: [
|
|
272
|
+
{ symbol: "SPY", candles: spy, signal: momentumSignal(), weight: 2, maxAllocationPct: 0.5 },
|
|
273
|
+
{ symbol: "QQQ", candles: qqq, signal: momentumSignal(), weight: 2, maxAllocationPct: 0.5 },
|
|
274
|
+
{ symbol: "IWM", candles: iwm, signal: momentumSignal(), weight: 1, maxAllocationPct: 0.3 },
|
|
275
|
+
],
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`result.eqSeries` includes `lockedCapital` and `availableCapital` at each realized equity point. Use those to see how often the portfolio was fully deployed versus sitting partially idle.
|
|
280
|
+
|
|
281
|
+
<small>[Back to main page](README.md)</small>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tradelab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"bin",
|
|
40
|
+
"docs",
|
|
40
41
|
"dist",
|
|
41
42
|
"src",
|
|
42
43
|
"types",
|