tradelab 1.1.0 → 1.2.1
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/CHANGELOG.md +57 -0
- package/README.md +183 -373
- package/dist/cjs/index.cjs +39 -12
- package/dist/cjs/live.cjs +457 -18
- package/docs/README.md +32 -66
- package/docs/api-reference.md +269 -144
- package/docs/backtest-engine.md +167 -321
- package/docs/data-reporting-cli.md +114 -156
- package/docs/examples.md +6 -6
- package/docs/live-trading.md +254 -134
- package/docs/mcp.md +244 -23
- package/docs/research.md +99 -45
- package/examples/mcpLiveTrading.js +77 -0
- package/package.json +11 -3
- package/src/engine/optimize.js +25 -1
- package/src/engine/portfolio.js +6 -2
- package/src/live/dashboard/server.js +67 -8
- package/src/live/engine/paperEngine.js +21 -11
- package/src/live/index.js +2 -0
- package/src/live/session.js +439 -0
- package/src/mcp/liveTools.js +202 -0
- package/src/mcp/schemas.js +119 -0
- package/src/mcp/server.js +5 -1
- package/src/mcp/tools.js +125 -2
- package/src/research/monteCarlo.js +6 -2
- package/templates/dashboard.html +595 -108
- package/types/index.d.ts +25 -0
- package/types/live.d.ts +102 -1
- package/types/mcp.d.ts +17 -0
- package/docs/superpowers/plans/2026-00-overview.md +0 -101
- package/docs/superpowers/plans/2026-01-metrics-correctness.md +0 -873
- package/docs/superpowers/plans/2026-02-indicator-library.md +0 -677
- package/docs/superpowers/plans/2026-03-overfitting-toolkit.md +0 -882
- package/docs/superpowers/plans/2026-04-async-signals-seeding.md +0 -981
- package/docs/superpowers/plans/2026-05-mcp-server.md +0 -758
- package/docs/superpowers/plans/2026-06-parallel-param-sweep.md +0 -508
- package/docs/superpowers/plans/2026-07-funding-carry-costs.md +0 -535
- package/docs/superpowers/plans/2026-08-live-dashboard.md +0 -547
- package/docs/superpowers/plans/HANDOFF.md +0 -88
package/docs/backtest-engine.md
CHANGED
|
@@ -1,40 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Backtesting
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This guide covers the research engine: `backtest`, `backtestAsync`, `backtestTicks`, `backtestPortfolio`, `walkForwardOptimize`, `optimize`, and `buildMetrics`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Back to docs](README.md)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- `backtestAsync(options)`
|
|
9
|
-
- `backtestTicks(options)`
|
|
10
|
-
- `backtestPortfolio(options)`
|
|
11
|
-
- `walkForwardOptimize(options)`
|
|
12
|
-
- `buildMetrics(input)`
|
|
7
|
+
## Choose an Entry Point
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
| Use this when... | Call |
|
|
10
|
+
| --------------------------------------- | ------------------------------ |
|
|
11
|
+
| You have candles for one symbol | `backtest(options)` |
|
|
12
|
+
| Your signal returns a promise | `backtestAsync(options)` |
|
|
13
|
+
| You have tick or quote data | `backtestTicks(options)` |
|
|
14
|
+
| You want one result across many systems | `backtestPortfolio(options)` |
|
|
15
|
+
| You want rolling train/test validation | `walkForwardOptimize(options)` |
|
|
16
|
+
| You want a worker-pool parameter sweep | `optimize(options)` |
|
|
17
|
+
| You already have trades and equity data | `buildMetrics(input)` |
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
## Candle Shape
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## Choose the right function
|
|
21
|
-
|
|
22
|
-
| Use case | Function |
|
|
23
|
-
| ----------------------------------------- | ----------------------- |
|
|
24
|
-
| One strategy on one candle series | `backtest()` |
|
|
25
|
-
| Async or model-backed candle signal | `backtestAsync()` |
|
|
26
|
-
| One strategy on tick or quote data | `backtestTicks()` |
|
|
27
|
-
| Multiple symbols with one combined result | `backtestPortfolio()` |
|
|
28
|
-
| Rolling or anchored train/test validation | `walkForwardOptimize()` |
|
|
29
|
-
| Recompute metrics from realized trades | `buildMetrics()` |
|
|
30
|
-
|
|
31
|
-
## Candle input
|
|
32
|
-
|
|
33
|
-
Candles should be sorted in ascending time order.
|
|
21
|
+
Candles should be sorted oldest to newest.
|
|
34
22
|
|
|
35
23
|
```js
|
|
36
24
|
{
|
|
37
|
-
time: 1735828200000,
|
|
25
|
+
time: 1735828200000, // Unix milliseconds
|
|
38
26
|
open: 100,
|
|
39
27
|
high: 102,
|
|
40
28
|
low: 99,
|
|
@@ -43,105 +31,94 @@ Candles should be sorted in ascending time order.
|
|
|
43
31
|
}
|
|
44
32
|
```
|
|
45
33
|
|
|
46
|
-
The
|
|
47
|
-
|
|
48
|
-
## `backtest(options)`
|
|
49
|
-
|
|
50
|
-
`backtest()` is the main single-symbol entry point.
|
|
34
|
+
The data loaders normalize common aliases such as `timestamp`, `date`, `o`, `h`, `l`, and `c`.
|
|
51
35
|
|
|
52
|
-
|
|
36
|
+
## First Backtest
|
|
53
37
|
|
|
54
38
|
```js
|
|
55
39
|
import { backtest } from "tradelab";
|
|
56
40
|
|
|
57
41
|
const result = backtest({
|
|
58
42
|
candles,
|
|
59
|
-
|
|
60
|
-
|
|
43
|
+
symbol: "SPY",
|
|
44
|
+
interval: "1d",
|
|
45
|
+
equity: 10_000,
|
|
46
|
+
riskPct: 1,
|
|
47
|
+
warmupBars: 50,
|
|
48
|
+
signal({ bar, index, openPosition }) {
|
|
49
|
+
if (openPosition || index < 50) return null;
|
|
61
50
|
return {
|
|
62
51
|
side: "long",
|
|
63
|
-
|
|
64
|
-
stop: bar.close - 2,
|
|
52
|
+
stop: bar.close * 0.97,
|
|
65
53
|
rr: 2,
|
|
66
54
|
};
|
|
67
55
|
},
|
|
68
56
|
});
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Required fields
|
|
72
57
|
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
candles: Candle[],
|
|
76
|
-
signal: ({ candles, index, bar, equity, openPosition, pendingOrder }) => Signal | null
|
|
77
|
-
}
|
|
58
|
+
console.log(result.metrics);
|
|
78
59
|
```
|
|
79
60
|
|
|
80
|
-
|
|
61
|
+
Set these options first:
|
|
81
62
|
|
|
82
|
-
| Option
|
|
83
|
-
|
|
|
84
|
-
| `symbol
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `strict` | Throws on direct lookahead access such as `candles[index + 1]` |
|
|
91
|
-
| `costs` | Slippage, spread, and commission model |
|
|
63
|
+
| Option | Why it matters |
|
|
64
|
+
| ------------ | --------------------------------------------- |
|
|
65
|
+
| `symbol` | Labels results and exports |
|
|
66
|
+
| `interval` | Annualizes metrics correctly |
|
|
67
|
+
| `equity` | Starting account value |
|
|
68
|
+
| `riskPct` | Default risk per trade when `qty` is absent |
|
|
69
|
+
| `warmupBars` | Prevents indicators from trading before ready |
|
|
70
|
+
| `costs` | Keeps edge estimates from ignoring friction |
|
|
92
71
|
|
|
93
|
-
|
|
72
|
+
## Signal Contract
|
|
94
73
|
|
|
95
|
-
|
|
96
|
-
- `riskPct`
|
|
97
|
-
- `warmupBars`
|
|
98
|
-
- `flattenAtClose`
|
|
99
|
-
- `costs`
|
|
74
|
+
Every engine calls your strategy with the same shape:
|
|
100
75
|
|
|
101
|
-
|
|
76
|
+
```js
|
|
77
|
+
signal({ candles, index, bar, equity, openPosition, pendingOrder });
|
|
78
|
+
```
|
|
102
79
|
|
|
103
|
-
|
|
80
|
+
Return `null` to skip the bar. Return a signal object to enter.
|
|
104
81
|
|
|
105
|
-
<!-- prettier-ignore -->
|
|
106
82
|
```js
|
|
107
|
-
{
|
|
83
|
+
{
|
|
84
|
+
side: "long",
|
|
85
|
+
entry: 101.25,
|
|
86
|
+
stop: 99.75,
|
|
87
|
+
takeProfit: 104.25
|
|
88
|
+
}
|
|
108
89
|
```
|
|
109
90
|
|
|
110
|
-
|
|
91
|
+
You can omit `entry`; the engine uses the current close. You can also omit `takeProfit` when you provide `rr`.
|
|
111
92
|
|
|
112
93
|
```js
|
|
113
|
-
|
|
114
|
-
{ side: "long", stop: 99.75, rr: 2 }
|
|
115
|
-
|
|
116
|
-
// explicit targets
|
|
117
|
-
{ side: "long", entry: 101.25, stop: 99.75, takeProfit: 104.25 }
|
|
94
|
+
{ side: "short", stop: 105, rr: 2 }
|
|
118
95
|
```
|
|
119
96
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
| Field | Behavior |
|
|
123
|
-
| --------------------------- | ------------------------------------------------ |
|
|
124
|
-
| `side` | Accepts `long`, `short`, `buy`, or `sell` |
|
|
125
|
-
| `entry` | Defaults to the current close if omitted |
|
|
126
|
-
| `takeProfit` | Can be derived from `rr` or `_rr` |
|
|
127
|
-
| `qty` or `size` | Overrides risk-based sizing |
|
|
128
|
-
| `riskPct` or `riskFraction` | Overrides the global risk setting for that trade |
|
|
97
|
+
Useful signal fields:
|
|
129
98
|
|
|
130
|
-
|
|
99
|
+
| Field | Meaning |
|
|
100
|
+
| ---------------------------- | --------------------------------- |
|
|
101
|
+
| `side` | `long`, `short`, `buy`, or `sell` |
|
|
102
|
+
| `entry`, `limit`, `price` | Entry price aliases |
|
|
103
|
+
| `stop`, `stopLoss`, `sl` | Stop price aliases |
|
|
104
|
+
| `takeProfit`, `target`, `tp` | Target price aliases |
|
|
105
|
+
| `rr` or `_rr` | Target in R multiples |
|
|
106
|
+
| `qty` or `size` | Fixed position size |
|
|
107
|
+
| `riskPct` or `riskFraction` | Per-trade risk override |
|
|
131
108
|
|
|
132
|
-
## Async
|
|
109
|
+
## Async Signals
|
|
133
110
|
|
|
134
|
-
Use `backtestAsync()` when
|
|
111
|
+
Use `backtestAsync()` when your signal waits on a model, service, file read, or other async work.
|
|
135
112
|
|
|
136
113
|
```js
|
|
137
114
|
import { backtestAsync, LlmSignal } from "tradelab";
|
|
138
115
|
|
|
139
|
-
const
|
|
116
|
+
const modelSignal = new LlmSignal({
|
|
140
117
|
budgetMs: 2000,
|
|
141
118
|
onError: "skip",
|
|
142
119
|
async resolve({ candles, bar }) {
|
|
143
|
-
const recent = candles.slice(-
|
|
144
|
-
return recent.
|
|
120
|
+
const recent = candles.slice(-10);
|
|
121
|
+
return recent.at(-1).close > recent[0].close
|
|
145
122
|
? { side: "long", stop: bar.close * 0.98, rr: 2 }
|
|
146
123
|
: null;
|
|
147
124
|
},
|
|
@@ -149,48 +126,21 @@ const llm = new LlmSignal({
|
|
|
149
126
|
|
|
150
127
|
const result = await backtestAsync({
|
|
151
128
|
candles,
|
|
152
|
-
signal:
|
|
129
|
+
signal: modelSignal.signal,
|
|
153
130
|
signalBudgetMs: 3000,
|
|
154
131
|
});
|
|
155
132
|
```
|
|
156
133
|
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
`LlmSignal` is an optional wrapper for model-backed decisions:
|
|
160
|
-
|
|
161
|
-
- caches by bar time, so repeated calls for one bar reuse the same decision
|
|
162
|
-
- passes a no-lookahead candle view into `resolve()`
|
|
163
|
-
- enforces `budgetMs` with the same timeout primitive as `backtestAsync()`
|
|
164
|
-
- records each result or error in `llm.log`
|
|
165
|
-
- returns `null` on errors by default, or rethrows with `onError: "throw"`
|
|
166
|
-
|
|
167
|
-
Live trading also awaits async signals; see [live trading](live-trading.md).
|
|
168
|
-
|
|
169
|
-
### Optional per-trade hints
|
|
170
|
-
|
|
171
|
-
These values are read from the signal object when present:
|
|
172
|
-
|
|
173
|
-
- `_entryExpiryBars`
|
|
174
|
-
- `_cooldownBars`
|
|
175
|
-
- `_breakevenAtR`
|
|
176
|
-
- `_trailAfterR`
|
|
177
|
-
- `_maxBarsInTrade`
|
|
178
|
-
- `_maxHoldMin`
|
|
179
|
-
- `_rr`
|
|
180
|
-
- `_initRisk`
|
|
181
|
-
- `_imb`
|
|
134
|
+
`LlmSignal` caches one decision per bar, blocks lookahead access, records decisions in `log`, and can either skip or throw on errors.
|
|
182
135
|
|
|
183
|
-
|
|
136
|
+
## Costs
|
|
184
137
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
- `slippageBps`
|
|
188
|
-
- `feeBps`
|
|
189
|
-
|
|
190
|
-
For more control, use `costs`:
|
|
138
|
+
The old top-level `slippageBps` and `feeBps` options still work. Prefer `costs` for new work:
|
|
191
139
|
|
|
192
140
|
```js
|
|
193
|
-
{
|
|
141
|
+
const result = backtest({
|
|
142
|
+
candles,
|
|
143
|
+
signal,
|
|
194
144
|
costs: {
|
|
195
145
|
slippageBps: 2,
|
|
196
146
|
spreadBps: 1,
|
|
@@ -200,7 +150,6 @@ For more control, use `costs`:
|
|
|
200
150
|
stop: 4,
|
|
201
151
|
},
|
|
202
152
|
commissionBps: 1,
|
|
203
|
-
commissionPerUnit: 0,
|
|
204
153
|
commissionPerOrder: 1,
|
|
205
154
|
minCommission: 1,
|
|
206
155
|
carry: {
|
|
@@ -213,277 +162,174 @@ For more control, use `costs`:
|
|
|
213
162
|
anchorMs: 0,
|
|
214
163
|
},
|
|
215
164
|
},
|
|
216
|
-
}
|
|
165
|
+
});
|
|
217
166
|
```
|
|
218
167
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
- slippage is applied in trade direction
|
|
222
|
-
- spread is modeled as half-spread paid on entry and exit
|
|
223
|
-
- commission can be percentage-based, per-unit, per-order, or mixed
|
|
224
|
-
- `minCommission` floors the fee for that fill
|
|
225
|
-
- `carry.longAnnualBps` and `carry.shortAnnualBps` are annualized financing or borrow rates deducted when each leg closes
|
|
226
|
-
- `funding.rateBps` applies once per funding boundary in `(openTime, closeTime]`; positive rates charge longs and credit shorts
|
|
227
|
-
|
|
228
|
-
This is still a bar-based simulation. It does not model queue position, exchange microstructure, or realistic intrabar order priority.
|
|
229
|
-
|
|
230
|
-
Closed trades expose the time-based charge as `trade.exit.financing`. It is already included in `trade.exit.pnl` and aggregate metrics, so use it only when you need attribution.
|
|
231
|
-
|
|
232
|
-
### Advanced trade management
|
|
168
|
+
How the cost model works:
|
|
233
169
|
|
|
234
|
-
|
|
170
|
+
- slippage is applied in the trade direction
|
|
171
|
+
- spread is paid as half-spread on entry and exit
|
|
172
|
+
- commission can be bps-based, per-unit, per-order, or mixed
|
|
173
|
+
- `minCommission` applies per fill
|
|
174
|
+
- carry is annualized and deducted when a leg closes
|
|
175
|
+
- funding applies at boundaries in `(openTime, closeTime]`
|
|
176
|
+
- positive funding charges longs and credits shorts
|
|
235
177
|
|
|
236
|
-
|
|
237
|
-
- `maxDailyLossPct`, `dailyMaxTrades`, `postLossCooldownBars`
|
|
238
|
-
- `atrTrailMult`, `atrTrailPeriod`
|
|
239
|
-
- `mfeTrail`
|
|
240
|
-
- `pyramiding`
|
|
241
|
-
- `volScale`
|
|
242
|
-
- `entryChase`
|
|
243
|
-
- `qtyStep`, `minQty`, `maxLeverage`
|
|
244
|
-
- `reanchorStopOnFill`, `maxSlipROnFill`
|
|
245
|
-
- `oco`
|
|
246
|
-
- `triggerMode`
|
|
178
|
+
Closed trades include `exit.financing` when carry or funding applies. It is already included in `exit.pnl`.
|
|
247
179
|
|
|
248
|
-
|
|
180
|
+
## Result Shape
|
|
249
181
|
|
|
250
|
-
1. Start with `entry`, `stop`, and `rr`
|
|
251
|
-
2. Add `costs`
|
|
252
|
-
3. Add trailing, scale-outs, or pyramiding only if the real strategy uses them
|
|
253
|
-
|
|
254
|
-
## Result shape
|
|
255
|
-
|
|
256
|
-
`backtest()` returns an object with these fields:
|
|
257
|
-
|
|
258
|
-
<!-- prettier-ignore -->
|
|
259
182
|
```js
|
|
260
|
-
{
|
|
183
|
+
{
|
|
184
|
+
(symbol,
|
|
185
|
+
interval,
|
|
186
|
+
range,
|
|
187
|
+
trades, // every realized leg
|
|
188
|
+
positions, // completed positions
|
|
189
|
+
openPositions, // still open at the end
|
|
190
|
+
metrics,
|
|
191
|
+
eqSeries,
|
|
192
|
+
replay);
|
|
193
|
+
}
|
|
261
194
|
```
|
|
262
195
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
Every realized leg, including partial exits and scale-outs.
|
|
266
|
-
|
|
267
|
-
### `positions`
|
|
268
|
-
|
|
269
|
-
Completed positions only. This is the collection most users want for top-line analysis.
|
|
270
|
-
|
|
271
|
-
If you are unsure whether to use `trades` or `positions`, start with `positions`.
|
|
272
|
-
|
|
273
|
-
### `metrics`
|
|
274
|
-
|
|
275
|
-
Most users start with:
|
|
276
|
-
|
|
277
|
-
- `trades`
|
|
278
|
-
- `winRate`
|
|
279
|
-
- `expectancy`
|
|
280
|
-
- `profitFactor`
|
|
281
|
-
- `maxDrawdown`
|
|
282
|
-
- `sharpe`
|
|
283
|
-
- `avgHold`
|
|
284
|
-
- `returnPct`
|
|
285
|
-
- `totalPnL`
|
|
286
|
-
- `finalEquity`
|
|
287
|
-
- `sideBreakdown`
|
|
196
|
+
Use `positions` for normal trade analysis. Use `trades` when you need partial exits or scale-out legs.
|
|
288
197
|
|
|
289
|
-
|
|
198
|
+
Important metrics:
|
|
290
199
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
200
|
+
| Metric | Meaning |
|
|
201
|
+
| ---------------------- | ----------------------------------------------------------------------------------- |
|
|
202
|
+
| `trades` | Number of completed positions |
|
|
203
|
+
| `winRate` | Winning completed positions / all positions |
|
|
204
|
+
| `profitFactor` | Gross profit / gross loss |
|
|
205
|
+
| `totalPnL` | Realized PnL |
|
|
206
|
+
| `returnPct` | Return on starting equity |
|
|
207
|
+
| `maxDrawdown` | Max drawdown as a decimal |
|
|
208
|
+
| `sharpeDaily` | Daily-bucketed Sharpe |
|
|
209
|
+
| `sharpeAnnualized` | Annualized Sharpe |
|
|
210
|
+
| `annualizationPeriods` | Periods used for annualization |
|
|
211
|
+
| `sideBreakdown` | Long and short side summaries |
|
|
212
|
+
| `benchmark` | Alpha, beta, correlation, and information ratio when benchmark returns are supplied |
|
|
295
213
|
|
|
296
|
-
|
|
214
|
+
Ratios are clamped to finite numbers before returning, so exported JSON does not contain `Infinity` or `NaN`.
|
|
297
215
|
|
|
298
|
-
|
|
299
|
-
- `metrics.profitFactor`: whether winners beat losers gross of the chosen fill model
|
|
300
|
-
- `metrics.maxDrawdown`: whether the path is survivable
|
|
301
|
-
- `metrics.sideBreakdown`: whether one side carries the result
|
|
216
|
+
## Tick Backtests
|
|
302
217
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
- `sharpe` / `sortino` are per-period (daily-bucketed).
|
|
306
|
-
- `sharpeAnnualized` / `sortinoAnnualized` scale by `sqrt(annualizationPeriods)`,
|
|
307
|
-
where `annualizationPeriods` is derived from `interval` (falling back to the
|
|
308
|
-
median bar spacing). Use these to compare strategies across timeframes.
|
|
309
|
-
- `profitFactor`, `calmar`, and the Sharpe/Sortino family are clamped to a finite
|
|
310
|
-
`BIG_NUMBER` (1e9) so `metrics` JSON never contains `Infinity` or `NaN`.
|
|
311
|
-
- `benchmark` (`{ alpha, beta, correlation, informationRatio, trackingError }`)
|
|
312
|
-
is populated when you pass `benchmarkReturns` (per-day return array aligned to
|
|
313
|
-
the strategy's daily equity buckets) to `backtest()`.
|
|
314
|
-
|
|
315
|
-
### `eqSeries`
|
|
316
|
-
|
|
317
|
-
Realized equity points:
|
|
218
|
+
`backtestTicks()` uses event-style tick or quote rows:
|
|
318
219
|
|
|
319
220
|
```js
|
|
320
|
-
|
|
221
|
+
const result = backtestTicks({
|
|
222
|
+
ticks,
|
|
223
|
+
symbol: "BTC-USD",
|
|
224
|
+
signal,
|
|
225
|
+
queueFillProbability: 0.4,
|
|
226
|
+
seed: "btc-run-1",
|
|
227
|
+
});
|
|
321
228
|
```
|
|
322
229
|
|
|
323
|
-
`time` and `
|
|
324
|
-
|
|
325
|
-
### `replay`
|
|
326
|
-
|
|
327
|
-
Visualization payload:
|
|
328
|
-
|
|
329
|
-
```js
|
|
330
|
-
{
|
|
331
|
-
frames: [{ t, price, equity, posSide, posSize }],
|
|
332
|
-
events: [{ t, price, type, side, size, tradeId, reason, pnl }]
|
|
333
|
-
}
|
|
334
|
-
```
|
|
230
|
+
Supported tick fields include `time`, `price`, `last`, `bid`, `ask`, `high`, `low`, `size`, and `volume`. Market entries fill on the next tick. Limit orders can fill at touch based on `queueFillProbability`. Stops use the stop-specific slippage model when provided.
|
|
335
231
|
|
|
336
|
-
|
|
232
|
+
Use `seed` to make probabilistic queue fills reproducible.
|
|
337
233
|
|
|
338
|
-
##
|
|
234
|
+
## Portfolio Backtests
|
|
339
235
|
|
|
340
|
-
|
|
236
|
+
`backtestPortfolio()` runs multiple systems against one shared account.
|
|
341
237
|
|
|
342
238
|
```js
|
|
343
239
|
const result = backtestPortfolio({
|
|
344
240
|
equity: 100_000,
|
|
241
|
+
interval: "1d",
|
|
242
|
+
maxDailyLossPct: 3,
|
|
345
243
|
systems: [
|
|
346
|
-
{ symbol: "SPY", candles: spy, signal:
|
|
347
|
-
{ symbol: "QQQ", candles: qqq, signal:
|
|
244
|
+
{ symbol: "SPY", candles: spy, signal: spySignal, weight: 2 },
|
|
245
|
+
{ symbol: "QQQ", candles: qqq, signal: qqqSignal, weight: 1 },
|
|
348
246
|
],
|
|
349
247
|
});
|
|
350
248
|
```
|
|
351
249
|
|
|
352
|
-
|
|
250
|
+
Weights are default allocation caps, not pre-funded sleeves. Capital is locked when a fill happens, and later fills size against what remains available.
|
|
353
251
|
|
|
354
|
-
|
|
355
|
-
- `weight` or `allocation: "equal" | "weight"` defines the default per-system cap, not a pre-funded sleeve
|
|
356
|
-
- fills lock capital immediately, later fills size against remaining available capital
|
|
357
|
-
- `eqSeries` points include `lockedCapital` and `availableCapital`
|
|
358
|
-
- `maxDailyLossPct` can halt all systems for the rest of the day once breached
|
|
252
|
+
Portfolio result extras:
|
|
359
253
|
|
|
360
|
-
|
|
254
|
+
- `systems`: per-system backtest results
|
|
255
|
+
- `eqSeries[].lockedCapital`
|
|
256
|
+
- `eqSeries[].availableCapital`
|
|
257
|
+
- portfolio-level `metrics`
|
|
361
258
|
|
|
362
|
-
|
|
363
|
-
- a prime-broker margin model
|
|
364
|
-
- a full portfolio optimizer
|
|
259
|
+
## Walk-Forward Validation
|
|
365
260
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
## `backtestTicks(options)`
|
|
369
|
-
|
|
370
|
-
Use tick mode when you want event-driven fills while keeping the same result shape as `backtest()`.
|
|
371
|
-
|
|
372
|
-
```js
|
|
373
|
-
const result = backtestTicks({
|
|
374
|
-
ticks,
|
|
375
|
-
queueFillProbability: 0.5,
|
|
376
|
-
seed: "experiment-42",
|
|
377
|
-
signal,
|
|
378
|
-
});
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
### How it works
|
|
382
|
-
|
|
383
|
-
- market entries fill on the next tick
|
|
384
|
-
- limit orders can fill at the touch based on `queueFillProbability`
|
|
385
|
-
- identical `seed` + data + options produce identical probabilistic limit-fill outcomes
|
|
386
|
-
- stop exits fill at the stop and use the normal stop slippage model from `costs.slippageByKind.stop`
|
|
387
|
-
- results still come back as `trades`, `positions`, `metrics`, `eqSeries`, and `replay`
|
|
388
|
-
|
|
389
|
-
## `walkForwardOptimize(options)`
|
|
390
|
-
|
|
391
|
-
Use walk-forward mode when one in-sample backtest is not enough and you want rolling or anchored train/test validation.
|
|
261
|
+
Use `walkForwardOptimize()` to reduce the risk of choosing parameters that only worked in-sample.
|
|
392
262
|
|
|
393
263
|
```js
|
|
394
264
|
const wf = walkForwardOptimize({
|
|
395
265
|
candles,
|
|
396
|
-
mode: "anchored",
|
|
397
266
|
trainBars: 180,
|
|
398
267
|
testBars: 60,
|
|
399
268
|
stepBars: 60,
|
|
269
|
+
mode: "anchored",
|
|
400
270
|
scoreBy: "profitFactor",
|
|
401
|
-
parameterSets
|
|
402
|
-
{ fast: 8, slow: 21, rr: 2 },
|
|
403
|
-
{ fast: 10, slow: 30, rr: 2 },
|
|
404
|
-
],
|
|
271
|
+
parameterSets,
|
|
405
272
|
signalFactory(params) {
|
|
406
|
-
return
|
|
273
|
+
return createSignal(params);
|
|
407
274
|
},
|
|
408
275
|
});
|
|
409
276
|
```
|
|
410
277
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
1. Evaluate every parameter set on the training slice
|
|
414
|
-
2. Pick the best one by `scoreBy`
|
|
415
|
-
3. Run that parameter set on the next test slice
|
|
416
|
-
4. Repeat for each window using either rolling or anchored training windows
|
|
417
|
-
|
|
418
|
-
### Return value
|
|
278
|
+
For each window, tradelab:
|
|
419
279
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
280
|
+
1. scores every parameter set on the training slice
|
|
281
|
+
2. chooses the winner
|
|
282
|
+
3. runs that winner on the test slice
|
|
283
|
+
4. reports aggregate metrics and winner stability
|
|
424
284
|
|
|
425
|
-
|
|
285
|
+
Read `wf.windows` before trusting `wf.metrics`. A strategy that changes winners every window is less convincing than one with stable winners.
|
|
426
286
|
|
|
427
|
-
##
|
|
287
|
+
## Parallel Parameter Sweeps
|
|
428
288
|
|
|
429
|
-
|
|
289
|
+
`optimize()` runs independent parameter sets in worker threads.
|
|
430
290
|
|
|
431
291
|
```js
|
|
432
|
-
import path from "node:path";
|
|
433
292
|
import { optimize, grid } from "tradelab";
|
|
434
293
|
|
|
435
294
|
const out = await optimize({
|
|
436
295
|
candles,
|
|
437
296
|
interval: "1d",
|
|
438
|
-
signalModulePath:
|
|
439
|
-
parameterSets: grid({
|
|
297
|
+
signalModulePath: new URL("../strategies/ema.js", import.meta.url).pathname,
|
|
298
|
+
parameterSets: grid({
|
|
299
|
+
fast: [8, 10, 12],
|
|
300
|
+
slow: [30, 50],
|
|
301
|
+
}),
|
|
440
302
|
concurrency: 4,
|
|
441
303
|
scoreBy: "sharpeAnnualized",
|
|
442
304
|
});
|
|
443
|
-
|
|
444
|
-
console.log(out.best?.params, out.best?.metrics);
|
|
445
305
|
```
|
|
446
306
|
|
|
447
|
-
|
|
307
|
+
The strategy module should export `createSignal(params)`.
|
|
448
308
|
|
|
449
309
|
```js
|
|
450
310
|
export function createSignal(params) {
|
|
451
311
|
return function signal(context) {
|
|
452
|
-
return null
|
|
312
|
+
// return null or a trade signal
|
|
453
313
|
};
|
|
454
314
|
}
|
|
455
315
|
```
|
|
456
316
|
|
|
457
|
-
Functions cannot cross
|
|
317
|
+
Functions cannot cross worker boundaries, so the worker receives a module path plus JSON-like parameter objects.
|
|
318
|
+
|
|
319
|
+
## Recomputing Metrics
|
|
458
320
|
|
|
459
|
-
|
|
321
|
+
Use `buildMetrics()` if you have realized trades and an equity curve from another process.
|
|
460
322
|
|
|
461
323
|
```js
|
|
462
|
-
{
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
324
|
+
const metrics = buildMetrics({
|
|
325
|
+
closed: trades,
|
|
326
|
+
equityStart: 10_000,
|
|
327
|
+
equityFinal: 11_250,
|
|
328
|
+
candles,
|
|
329
|
+
estBarMs: 86_400_000,
|
|
330
|
+
eqSeries,
|
|
331
|
+
interval: "1d",
|
|
332
|
+
});
|
|
467
333
|
```
|
|
468
334
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
`optimize()` is ESM-only in this release because it starts an ESM `worker_threads` worker via `import.meta.url`. Use it from ESM code, for example `node examples/optimize.js`.
|
|
472
|
-
|
|
473
|
-
## `buildMetrics(input)`
|
|
474
|
-
|
|
475
|
-
Most users do not need this directly. Use it when:
|
|
476
|
-
|
|
477
|
-
- you generate realized trades outside `backtest()`
|
|
478
|
-
- you filter a result and want fresh metrics
|
|
479
|
-
- you combine results manually
|
|
480
|
-
|
|
481
|
-
## Common mistakes
|
|
482
|
-
|
|
483
|
-
- using unsorted candles or mixed intervals in one series
|
|
484
|
-
- reading `trades` as if they were always full positions
|
|
485
|
-
- leaving costs at zero and overestimating edge
|
|
486
|
-
- trusting one backtest without out-of-sample validation
|
|
487
|
-
- debugging a strategy with `strict: false` when lookahead is possible
|
|
488
|
-
|
|
489
|
-
<small>[Back to main page](README.md)</small>
|
|
335
|
+
[Back to docs](README.md)
|