tradelab 1.0.1 → 1.2.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/CHANGELOG.md +112 -0
- package/README.md +188 -328
- package/bin/tradelab-mcp.js +7 -0
- package/bin/tradelab.js +29 -0
- package/dist/cjs/data.cjs +149 -26
- package/dist/cjs/index.cjs +1917 -1005
- package/dist/cjs/live.cjs +536 -25
- package/dist/cjs/ta.cjs +339 -0
- package/docs/README.md +32 -66
- package/docs/api-reference.md +283 -112
- package/docs/backtest-engine.md +210 -252
- package/docs/data-reporting-cli.md +114 -156
- package/docs/examples.md +6 -6
- package/docs/live-trading.md +263 -92
- package/docs/mcp.md +285 -0
- package/docs/research.md +157 -0
- package/examples/liveDashboard.js +33 -0
- package/examples/llmSignal.js +33 -0
- package/examples/mcpLiveTrading.js +77 -0
- package/examples/optimize.js +25 -0
- package/package.json +26 -4
- package/src/engine/asyncSignal.js +28 -0
- package/src/engine/backtest.js +13 -1
- package/src/engine/backtestAsync.js +27 -0
- package/src/engine/backtestTicks.js +13 -2
- package/src/engine/barSystemRunner.js +96 -41
- package/src/engine/execution.js +39 -0
- package/src/engine/grid.js +15 -0
- package/src/engine/llmSignal.js +84 -0
- package/src/engine/optimize.js +110 -0
- package/src/engine/optimizeWorker.js +67 -0
- package/src/engine/portfolio.js +4 -1
- package/src/engine/walkForward.js +1 -0
- package/src/index.js +9 -0
- package/src/live/dashboard/server.js +179 -0
- package/src/live/engine/liveEngine.js +2 -2
- package/src/live/engine/paperEngine.js +5 -0
- package/src/live/index.js +3 -0
- package/src/live/session.js +402 -0
- package/src/mcp/liveTools.js +179 -0
- package/src/mcp/schemas.js +167 -0
- package/src/mcp/server.js +35 -0
- package/src/mcp/tools.js +265 -0
- package/src/metrics/annualize.js +32 -0
- package/src/metrics/benchmark.js +55 -0
- package/src/metrics/buildMetrics.js +34 -13
- package/src/metrics/finite.js +17 -0
- package/src/research/combinations.js +18 -0
- package/src/research/cpcv.js +47 -0
- package/src/research/deflatedSharpe.js +35 -0
- package/src/research/index.js +6 -0
- package/src/research/monteCarlo.js +88 -0
- package/src/research/pbo.js +69 -0
- package/src/research/stats.js +78 -0
- package/src/strategies/builtins.js +96 -0
- package/src/strategies/index.js +30 -0
- package/src/ta/channels.js +67 -0
- package/src/ta/index.js +16 -0
- package/src/ta/oscillators.js +70 -0
- package/src/ta/trend.js +78 -0
- package/src/utils/random.js +33 -0
- package/templates/dashboard.html +661 -0
- package/types/index.d.ts +179 -0
- package/types/live.d.ts +114 -0
- package/types/mcp.d.ts +17 -0
- package/types/ta.d.ts +45 -0
package/docs/backtest-engine.md
CHANGED
|
@@ -1,38 +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
|
-
- `backtestTicks(options)`
|
|
9
|
-
- `backtestPortfolio(options)`
|
|
10
|
-
- `walkForwardOptimize(options)`
|
|
11
|
-
- `buildMetrics(input)`
|
|
7
|
+
## Choose an Entry Point
|
|
12
8
|
|
|
13
|
-
|
|
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)` |
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
## Candle Shape
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## Choose the right function
|
|
20
|
-
|
|
21
|
-
| Use case | Function |
|
|
22
|
-
| ----------------------------------------- | ----------------------- |
|
|
23
|
-
| One strategy on one candle series | `backtest()` |
|
|
24
|
-
| One strategy on tick or quote data | `backtestTicks()` |
|
|
25
|
-
| Multiple symbols with one combined result | `backtestPortfolio()` |
|
|
26
|
-
| Rolling or anchored train/test validation | `walkForwardOptimize()` |
|
|
27
|
-
| Recompute metrics from realized trades | `buildMetrics()` |
|
|
28
|
-
|
|
29
|
-
## Candle input
|
|
30
|
-
|
|
31
|
-
Candles should be sorted in ascending time order.
|
|
21
|
+
Candles should be sorted oldest to newest.
|
|
32
22
|
|
|
33
23
|
```js
|
|
34
24
|
{
|
|
35
|
-
time: 1735828200000,
|
|
25
|
+
time: 1735828200000, // Unix milliseconds
|
|
36
26
|
open: 100,
|
|
37
27
|
high: 102,
|
|
38
28
|
low: 99,
|
|
@@ -41,117 +31,116 @@ Candles should be sorted in ascending time order.
|
|
|
41
31
|
}
|
|
42
32
|
```
|
|
43
33
|
|
|
44
|
-
The
|
|
45
|
-
|
|
46
|
-
## `backtest(options)`
|
|
47
|
-
|
|
48
|
-
`backtest()` is the main single-symbol entry point.
|
|
34
|
+
The data loaders normalize common aliases such as `timestamp`, `date`, `o`, `h`, `l`, and `c`.
|
|
49
35
|
|
|
50
|
-
|
|
36
|
+
## First Backtest
|
|
51
37
|
|
|
52
38
|
```js
|
|
53
39
|
import { backtest } from "tradelab";
|
|
54
40
|
|
|
55
41
|
const result = backtest({
|
|
56
42
|
candles,
|
|
57
|
-
|
|
58
|
-
|
|
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;
|
|
59
50
|
return {
|
|
60
51
|
side: "long",
|
|
61
|
-
|
|
62
|
-
stop: bar.close - 2,
|
|
52
|
+
stop: bar.close * 0.97,
|
|
63
53
|
rr: 2,
|
|
64
54
|
};
|
|
65
55
|
},
|
|
66
56
|
});
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### Required fields
|
|
70
57
|
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
candles: Candle[],
|
|
74
|
-
signal: ({ candles, index, bar, equity, openPosition, pendingOrder }) => Signal | null
|
|
75
|
-
}
|
|
58
|
+
console.log(result.metrics);
|
|
76
59
|
```
|
|
77
60
|
|
|
78
|
-
|
|
61
|
+
Set these options first:
|
|
79
62
|
|
|
80
|
-
| Option
|
|
81
|
-
|
|
|
82
|
-
| `symbol
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `strict` | Throws on direct lookahead access such as `candles[index + 1]` |
|
|
89
|
-
| `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 |
|
|
90
71
|
|
|
91
|
-
|
|
72
|
+
## Signal Contract
|
|
92
73
|
|
|
93
|
-
|
|
94
|
-
- `riskPct`
|
|
95
|
-
- `warmupBars`
|
|
96
|
-
- `flattenAtClose`
|
|
97
|
-
- `costs`
|
|
74
|
+
Every engine calls your strategy with the same shape:
|
|
98
75
|
|
|
99
|
-
|
|
76
|
+
```js
|
|
77
|
+
signal({ candles, index, bar, equity, openPosition, pendingOrder });
|
|
78
|
+
```
|
|
100
79
|
|
|
101
|
-
|
|
80
|
+
Return `null` to skip the bar. Return a signal object to enter.
|
|
102
81
|
|
|
103
|
-
<!-- prettier-ignore -->
|
|
104
82
|
```js
|
|
105
|
-
{
|
|
83
|
+
{
|
|
84
|
+
side: "long",
|
|
85
|
+
entry: 101.25,
|
|
86
|
+
stop: 99.75,
|
|
87
|
+
takeProfit: 104.25
|
|
88
|
+
}
|
|
106
89
|
```
|
|
107
90
|
|
|
108
|
-
|
|
91
|
+
You can omit `entry`; the engine uses the current close. You can also omit `takeProfit` when you provide `rr`.
|
|
109
92
|
|
|
110
93
|
```js
|
|
111
|
-
|
|
112
|
-
{ side: "long", stop: 99.75, rr: 2 }
|
|
113
|
-
|
|
114
|
-
// explicit targets
|
|
115
|
-
{ side: "long", entry: 101.25, stop: 99.75, takeProfit: 104.25 }
|
|
94
|
+
{ side: "short", stop: 105, rr: 2 }
|
|
116
95
|
```
|
|
117
96
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
| Field | Behavior |
|
|
121
|
-
| --------------------------- | ------------------------------------------------ |
|
|
122
|
-
| `side` | Accepts `long`, `short`, `buy`, or `sell` |
|
|
123
|
-
| `entry` | Defaults to the current close if omitted |
|
|
124
|
-
| `takeProfit` | Can be derived from `rr` or `_rr` |
|
|
125
|
-
| `qty` or `size` | Overrides risk-based sizing |
|
|
126
|
-
| `riskPct` or `riskFraction` | Overrides the global risk setting for that trade |
|
|
97
|
+
Useful signal fields:
|
|
127
98
|
|
|
128
|
-
|
|
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 |
|
|
129
108
|
|
|
130
|
-
|
|
109
|
+
## Async Signals
|
|
131
110
|
|
|
132
|
-
|
|
111
|
+
Use `backtestAsync()` when your signal waits on a model, service, file read, or other async work.
|
|
133
112
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
113
|
+
```js
|
|
114
|
+
import { backtestAsync, LlmSignal } from "tradelab";
|
|
115
|
+
|
|
116
|
+
const modelSignal = new LlmSignal({
|
|
117
|
+
budgetMs: 2000,
|
|
118
|
+
onError: "skip",
|
|
119
|
+
async resolve({ candles, bar }) {
|
|
120
|
+
const recent = candles.slice(-10);
|
|
121
|
+
return recent.at(-1).close > recent[0].close
|
|
122
|
+
? { side: "long", stop: bar.close * 0.98, rr: 2 }
|
|
123
|
+
: null;
|
|
124
|
+
},
|
|
125
|
+
});
|
|
143
126
|
|
|
144
|
-
|
|
127
|
+
const result = await backtestAsync({
|
|
128
|
+
candles,
|
|
129
|
+
signal: modelSignal.signal,
|
|
130
|
+
signalBudgetMs: 3000,
|
|
131
|
+
});
|
|
132
|
+
```
|
|
145
133
|
|
|
146
|
-
|
|
134
|
+
`LlmSignal` caches one decision per bar, blocks lookahead access, records decisions in `log`, and can either skip or throw on errors.
|
|
147
135
|
|
|
148
|
-
|
|
149
|
-
- `feeBps`
|
|
136
|
+
## Costs
|
|
150
137
|
|
|
151
|
-
|
|
138
|
+
The old top-level `slippageBps` and `feeBps` options still work. Prefer `costs` for new work:
|
|
152
139
|
|
|
153
140
|
```js
|
|
154
|
-
{
|
|
141
|
+
const result = backtest({
|
|
142
|
+
candles,
|
|
143
|
+
signal,
|
|
155
144
|
costs: {
|
|
156
145
|
slippageBps: 2,
|
|
157
146
|
spreadBps: 1,
|
|
@@ -161,217 +150,186 @@ For more control, use `costs`:
|
|
|
161
150
|
stop: 4,
|
|
162
151
|
},
|
|
163
152
|
commissionBps: 1,
|
|
164
|
-
commissionPerUnit: 0,
|
|
165
153
|
commissionPerOrder: 1,
|
|
166
154
|
minCommission: 1,
|
|
155
|
+
carry: {
|
|
156
|
+
longAnnualBps: 500,
|
|
157
|
+
shortAnnualBps: 800,
|
|
158
|
+
},
|
|
159
|
+
funding: {
|
|
160
|
+
rateBps: 10,
|
|
161
|
+
intervalMs: 8 * 60 * 60 * 1000,
|
|
162
|
+
anchorMs: 0,
|
|
163
|
+
},
|
|
167
164
|
},
|
|
168
|
-
}
|
|
165
|
+
});
|
|
169
166
|
```
|
|
170
167
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
- slippage is applied in trade direction
|
|
174
|
-
- spread is modeled as half-spread paid on entry and exit
|
|
175
|
-
- commission can be percentage-based, per-unit, per-order, or mixed
|
|
176
|
-
- `minCommission` floors the fee for that fill
|
|
177
|
-
|
|
178
|
-
This is still a bar-based simulation. It does not model queue position, exchange microstructure, or realistic intrabar order priority.
|
|
168
|
+
How the cost model works:
|
|
179
169
|
|
|
180
|
-
|
|
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
|
|
181
177
|
|
|
182
|
-
|
|
178
|
+
Closed trades include `exit.financing` when carry or funding applies. It is already included in `exit.pnl`.
|
|
183
179
|
|
|
184
|
-
|
|
185
|
-
- `maxDailyLossPct`, `dailyMaxTrades`, `postLossCooldownBars`
|
|
186
|
-
- `atrTrailMult`, `atrTrailPeriod`
|
|
187
|
-
- `mfeTrail`
|
|
188
|
-
- `pyramiding`
|
|
189
|
-
- `volScale`
|
|
190
|
-
- `entryChase`
|
|
191
|
-
- `qtyStep`, `minQty`, `maxLeverage`
|
|
192
|
-
- `reanchorStopOnFill`, `maxSlipROnFill`
|
|
193
|
-
- `oco`
|
|
194
|
-
- `triggerMode`
|
|
180
|
+
## Result Shape
|
|
195
181
|
|
|
196
|
-
Recommended order of adoption:
|
|
197
|
-
|
|
198
|
-
1. Start with `entry`, `stop`, and `rr`
|
|
199
|
-
2. Add `costs`
|
|
200
|
-
3. Add trailing, scale-outs, or pyramiding only if the real strategy uses them
|
|
201
|
-
|
|
202
|
-
## Result shape
|
|
203
|
-
|
|
204
|
-
`backtest()` returns an object with these fields:
|
|
205
|
-
|
|
206
|
-
<!-- prettier-ignore -->
|
|
207
182
|
```js
|
|
208
|
-
{
|
|
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
|
+
}
|
|
209
194
|
```
|
|
210
195
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
Every realized leg, including partial exits and scale-outs.
|
|
214
|
-
|
|
215
|
-
### `positions`
|
|
216
|
-
|
|
217
|
-
Completed positions only. This is the collection most users want for top-line analysis.
|
|
218
|
-
|
|
219
|
-
If you are unsure whether to use `trades` or `positions`, start with `positions`.
|
|
220
|
-
|
|
221
|
-
### `metrics`
|
|
196
|
+
Use `positions` for normal trade analysis. Use `trades` when you need partial exits or scale-out legs.
|
|
222
197
|
|
|
223
|
-
|
|
198
|
+
Important metrics:
|
|
224
199
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 |
|
|
236
213
|
|
|
237
|
-
|
|
214
|
+
Ratios are clamped to finite numbers before returning, so exported JSON does not contain `Infinity` or `NaN`.
|
|
238
215
|
|
|
239
|
-
|
|
240
|
-
- `rDist` percentiles
|
|
241
|
-
- `holdDistMin` percentiles
|
|
242
|
-
- daily stats under `daily`
|
|
216
|
+
## Tick Backtests
|
|
243
217
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
- `metrics.trades`: enough sample size to care
|
|
247
|
-
- `metrics.profitFactor`: whether winners beat losers gross of the chosen fill model
|
|
248
|
-
- `metrics.maxDrawdown`: whether the path is survivable
|
|
249
|
-
- `metrics.sideBreakdown`: whether one side carries the result
|
|
250
|
-
|
|
251
|
-
### `eqSeries`
|
|
252
|
-
|
|
253
|
-
Realized equity points:
|
|
218
|
+
`backtestTicks()` uses event-style tick or quote rows:
|
|
254
219
|
|
|
255
220
|
```js
|
|
256
|
-
|
|
221
|
+
const result = backtestTicks({
|
|
222
|
+
ticks,
|
|
223
|
+
symbol: "BTC-USD",
|
|
224
|
+
signal,
|
|
225
|
+
queueFillProbability: 0.4,
|
|
226
|
+
seed: "btc-run-1",
|
|
227
|
+
});
|
|
257
228
|
```
|
|
258
229
|
|
|
259
|
-
`time` and `
|
|
260
|
-
|
|
261
|
-
### `replay`
|
|
262
|
-
|
|
263
|
-
Visualization payload:
|
|
264
|
-
|
|
265
|
-
```js
|
|
266
|
-
{
|
|
267
|
-
frames: [{ t, price, equity, posSide, posSize }],
|
|
268
|
-
events: [{ t, price, type, side, size, tradeId, reason, pnl }]
|
|
269
|
-
}
|
|
270
|
-
```
|
|
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.
|
|
271
231
|
|
|
272
|
-
|
|
232
|
+
Use `seed` to make probabilistic queue fills reproducible.
|
|
273
233
|
|
|
274
|
-
##
|
|
234
|
+
## Portfolio Backtests
|
|
275
235
|
|
|
276
|
-
|
|
236
|
+
`backtestPortfolio()` runs multiple systems against one shared account.
|
|
277
237
|
|
|
278
238
|
```js
|
|
279
239
|
const result = backtestPortfolio({
|
|
280
240
|
equity: 100_000,
|
|
241
|
+
interval: "1d",
|
|
242
|
+
maxDailyLossPct: 3,
|
|
281
243
|
systems: [
|
|
282
|
-
{ symbol: "SPY", candles: spy, signal:
|
|
283
|
-
{ symbol: "QQQ", candles: qqq, signal:
|
|
244
|
+
{ symbol: "SPY", candles: spy, signal: spySignal, weight: 2 },
|
|
245
|
+
{ symbol: "QQQ", candles: qqq, signal: qqqSignal, weight: 1 },
|
|
284
246
|
],
|
|
285
247
|
});
|
|
286
248
|
```
|
|
287
249
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
- systems share one live capital pool
|
|
291
|
-
- `weight` or `allocation: "equal" | "weight"` defines the default per-system cap, not a pre-funded sleeve
|
|
292
|
-
- fills lock capital immediately, later fills size against remaining available capital
|
|
293
|
-
- `eqSeries` points include `lockedCapital` and `availableCapital`
|
|
294
|
-
- `maxDailyLossPct` can halt all systems for the rest of the day once breached
|
|
295
|
-
|
|
296
|
-
### What it is not
|
|
297
|
-
|
|
298
|
-
- a cross-margin broker simulator
|
|
299
|
-
- a prime-broker margin model
|
|
300
|
-
- a full portfolio optimizer
|
|
301
|
-
|
|
302
|
-
This mode now does enforce shared capital and cross-system sizing, but it still uses the library's research-oriented execution assumptions rather than full broker accounting.
|
|
303
|
-
|
|
304
|
-
## `backtestTicks(options)`
|
|
305
|
-
|
|
306
|
-
Use tick mode when you want event-driven fills while keeping the same result shape as `backtest()`.
|
|
307
|
-
|
|
308
|
-
```js
|
|
309
|
-
const result = backtestTicks({
|
|
310
|
-
ticks,
|
|
311
|
-
queueFillProbability: 0.5,
|
|
312
|
-
signal,
|
|
313
|
-
});
|
|
314
|
-
```
|
|
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.
|
|
315
251
|
|
|
316
|
-
|
|
252
|
+
Portfolio result extras:
|
|
317
253
|
|
|
318
|
-
-
|
|
319
|
-
-
|
|
320
|
-
-
|
|
321
|
-
-
|
|
254
|
+
- `systems`: per-system backtest results
|
|
255
|
+
- `eqSeries[].lockedCapital`
|
|
256
|
+
- `eqSeries[].availableCapital`
|
|
257
|
+
- portfolio-level `metrics`
|
|
322
258
|
|
|
323
|
-
##
|
|
259
|
+
## Walk-Forward Validation
|
|
324
260
|
|
|
325
|
-
Use
|
|
261
|
+
Use `walkForwardOptimize()` to reduce the risk of choosing parameters that only worked in-sample.
|
|
326
262
|
|
|
327
263
|
```js
|
|
328
264
|
const wf = walkForwardOptimize({
|
|
329
265
|
candles,
|
|
330
|
-
mode: "anchored",
|
|
331
266
|
trainBars: 180,
|
|
332
267
|
testBars: 60,
|
|
333
268
|
stepBars: 60,
|
|
269
|
+
mode: "anchored",
|
|
334
270
|
scoreBy: "profitFactor",
|
|
335
|
-
parameterSets
|
|
336
|
-
{ fast: 8, slow: 21, rr: 2 },
|
|
337
|
-
{ fast: 10, slow: 30, rr: 2 },
|
|
338
|
-
],
|
|
271
|
+
parameterSets,
|
|
339
272
|
signalFactory(params) {
|
|
340
|
-
return
|
|
273
|
+
return createSignal(params);
|
|
341
274
|
},
|
|
342
275
|
});
|
|
343
276
|
```
|
|
344
277
|
|
|
345
|
-
|
|
278
|
+
For each window, tradelab:
|
|
279
|
+
|
|
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
|
|
284
|
+
|
|
285
|
+
Read `wf.windows` before trusting `wf.metrics`. A strategy that changes winners every window is less convincing than one with stable winners.
|
|
286
|
+
|
|
287
|
+
## Parallel Parameter Sweeps
|
|
346
288
|
|
|
347
|
-
|
|
348
|
-
2. Pick the best one by `scoreBy`
|
|
349
|
-
3. Run that parameter set on the next test slice
|
|
350
|
-
4. Repeat for each window using either rolling or anchored training windows
|
|
289
|
+
`optimize()` runs independent parameter sets in worker threads.
|
|
351
290
|
|
|
352
|
-
|
|
291
|
+
```js
|
|
292
|
+
import { optimize, grid } from "tradelab";
|
|
353
293
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
294
|
+
const out = await optimize({
|
|
295
|
+
candles,
|
|
296
|
+
interval: "1d",
|
|
297
|
+
signalModulePath: new URL("../strategies/ema.js", import.meta.url).pathname,
|
|
298
|
+
parameterSets: grid({
|
|
299
|
+
fast: [8, 10, 12],
|
|
300
|
+
slow: [30, 50],
|
|
301
|
+
}),
|
|
302
|
+
concurrency: 4,
|
|
303
|
+
scoreBy: "sharpeAnnualized",
|
|
304
|
+
});
|
|
305
|
+
```
|
|
358
306
|
|
|
359
|
-
|
|
307
|
+
The strategy module should export `createSignal(params)`.
|
|
360
308
|
|
|
361
|
-
|
|
309
|
+
```js
|
|
310
|
+
export function createSignal(params) {
|
|
311
|
+
return function signal(context) {
|
|
312
|
+
// return null or a trade signal
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
```
|
|
362
316
|
|
|
363
|
-
|
|
317
|
+
Functions cannot cross worker boundaries, so the worker receives a module path plus JSON-like parameter objects.
|
|
364
318
|
|
|
365
|
-
|
|
366
|
-
- you filter a result and want fresh metrics
|
|
367
|
-
- you combine results manually
|
|
319
|
+
## Recomputing Metrics
|
|
368
320
|
|
|
369
|
-
|
|
321
|
+
Use `buildMetrics()` if you have realized trades and an equity curve from another process.
|
|
370
322
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
323
|
+
```js
|
|
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
|
+
});
|
|
333
|
+
```
|
|
376
334
|
|
|
377
|
-
|
|
335
|
+
[Back to docs](README.md)
|