tradelab 0.1.2 → 0.3.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 +51 -15
- package/bin/tradelab.js +384 -0
- package/dist/cjs/data.cjs +1750 -0
- package/dist/cjs/index.cjs +2556 -0
- package/package.json +32 -6
- package/src/data/yahoo.js +40 -17
- package/src/engine/backtest.js +75 -24
- package/src/engine/execution.js +44 -7
- package/src/engine/portfolio.js +160 -0
- package/src/engine/walkForward.js +126 -0
- package/src/index.js +3 -0
- package/src/metrics/buildMetrics.js +32 -16
- package/src/reporting/exportBacktestArtifacts.js +13 -0
- package/src/reporting/exportMetricsJson.js +24 -0
- package/src/reporting/renderHtmlReport.js +26 -9
- package/types/data.d.ts +13 -0
- package/types/index.d.ts +570 -0
package/README.md
CHANGED
|
@@ -2,21 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
# tradelab
|
|
4
4
|
|
|
5
|
-
`tradelab` is a
|
|
6
|
-
-
|
|
7
|
-
-
|
|
5
|
+
`tradelab` is a Node.js backtesting toolkit for trading strategy research. It lets you:
|
|
6
|
+
- load candles from Yahoo Finance or CSV
|
|
7
|
+
- run candle-based backtests with sizing, exits, and risk controls
|
|
8
|
+
- export trades, metrics, and HTML reports
|
|
8
9
|
|
|
9
|
-
The package
|
|
10
|
+
The package is modular by design, so you can use just the parts you need: data loading, backtesting, reporting, or the utility layer on its own.
|
|
11
|
+
|
|
12
|
+
It is built for historical research and testing, not broker connectivity or live trading.
|
|
10
13
|
|
|
11
14
|
## Features
|
|
12
15
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
16
|
+
- Modular structure: use the full workflow or just the engine, data layer, reporting, or helpers
|
|
17
|
+
- Backtest engine with pending entries, OCO exits, scale-outs, pyramiding, cooldowns, daily loss limits, optional replay/equity capture, and configurable slippage/commission modeling
|
|
18
|
+
- Historical data loading from Yahoo Finance, with local caching to avoid repeated downloads
|
|
19
|
+
- CSV import for common OHLCV formats and custom column mappings
|
|
20
|
+
- Position-level and leg-level metrics, including drawdown, expectancy, hold-time stats, and side breakdowns
|
|
21
|
+
- Multi-symbol portfolio aggregation and rolling walk-forward optimization helpers
|
|
22
|
+
- HTML report export, metrics JSON export, and trade CSV export
|
|
23
|
+
- Utility indicators and session helpers for strategy development
|
|
24
|
+
- CLI entrypoint for fetching data and running quick backtests from the terminal
|
|
25
|
+
- TypeScript definitions for the public API
|
|
20
26
|
|
|
21
27
|
## Installation
|
|
22
28
|
|
|
@@ -26,6 +32,23 @@ npm install tradelab
|
|
|
26
32
|
|
|
27
33
|
Node `18+` is required.
|
|
28
34
|
|
|
35
|
+
## Importing
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### ESM (recommended)
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { backtest, getHistoricalCandles, ema } from "tradelab";
|
|
42
|
+
import { fetchHistorical } from "tradelab/data";
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### CommonJS
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const { backtest, getHistoricalCandles, ema } = require("tradelab");
|
|
49
|
+
const { fetchHistorical } = require("tradelab/data");
|
|
50
|
+
```
|
|
51
|
+
|
|
29
52
|
## Quick Start
|
|
30
53
|
|
|
31
54
|
```js
|
|
@@ -72,7 +95,7 @@ exportBacktestArtifacts({
|
|
|
72
95
|
|
|
73
96
|
## Getting Historical Data
|
|
74
97
|
|
|
75
|
-
The simplest entry point is `getHistoricalCandles()`.
|
|
98
|
+
The simplest entry point is `getHistoricalCandles()`. For most users, it is the only data-loading function you need.
|
|
76
99
|
|
|
77
100
|
### Yahoo Finance
|
|
78
101
|
|
|
@@ -154,6 +177,7 @@ Quality-of-life behavior:
|
|
|
154
177
|
- `takeProfit` can be omitted if `rr` or `_rr` is provided
|
|
155
178
|
- `qty` or `size` can override risk-based sizing
|
|
156
179
|
- `riskPct` or `riskFraction` can override the global risk setting per signal
|
|
180
|
+
- `strict: true` throws if the strategy directly accesses candles beyond the current index
|
|
157
181
|
|
|
158
182
|
Optional engine hints:
|
|
159
183
|
|
|
@@ -173,24 +197,27 @@ Optional engine hints:
|
|
|
173
197
|
|
|
174
198
|
- `trades`: every realized leg, including scale-outs
|
|
175
199
|
- `positions`: completed positions only
|
|
176
|
-
- `metrics`: aggregate
|
|
177
|
-
- `eqSeries`: realized equity history
|
|
200
|
+
- `metrics`: aggregate stats including `winRate`, `expectancy`, `profitFactor`, `maxDrawdown`, `sharpe`, `avgHold`, and `sideBreakdown`
|
|
201
|
+
- `eqSeries`: realized equity history as `{ time, timestamp, equity }`
|
|
178
202
|
- `replay`: chart-friendly frame and event data
|
|
179
203
|
|
|
180
204
|
## Main Exports
|
|
181
205
|
|
|
182
206
|
- `backtest(options)`
|
|
207
|
+
- `backtestPortfolio({ systems, equity })`
|
|
208
|
+
- `walkForwardOptimize({ candles, signalFactory, parameterSets, trainBars, testBars })`
|
|
183
209
|
- `backtestHistorical({ data, backtestOptions })`
|
|
184
210
|
- `getHistoricalCandles(options)`
|
|
185
211
|
- `fetchHistorical(symbol, interval, period)`
|
|
186
212
|
- `loadCandlesFromCSV(filePath, options)`
|
|
187
213
|
- `saveCandlesToCache(candles, meta)`
|
|
188
214
|
- `loadCandlesFromCache(symbol, interval, period, outDir)`
|
|
215
|
+
- `exportMetricsJSON({ result, outDir })`
|
|
189
216
|
- `exportBacktestArtifacts({ result, outDir })`
|
|
190
217
|
|
|
191
218
|
## Reports
|
|
192
219
|
|
|
193
|
-
The HTML report is self-contained apart from the Plotly CDN script. Report markup, CSS, and client-side chart code live under `templates
|
|
220
|
+
The HTML report is self-contained apart from the Plotly CDN script. Report markup, CSS, and client-side chart code live under `templates/`.
|
|
194
221
|
|
|
195
222
|
Export helpers default CSV output to completed positions. Use `csvSource: "trades"` if you want every realized leg in the CSV.
|
|
196
223
|
|
|
@@ -201,8 +228,17 @@ node examples/emaCross.js
|
|
|
201
228
|
node examples/yahooEmaCross.js SPY 1d 1y
|
|
202
229
|
```
|
|
203
230
|
|
|
231
|
+
## CLI
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
npx tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
|
|
235
|
+
npx tradelab backtest --source csv --csvPath ./data/btc.csv --strategy buy-hold --holdBars 3
|
|
236
|
+
npx tradelab walk-forward --source yahoo --symbol QQQ --interval 1d --period 2y --trainBars 180 --testBars 60
|
|
237
|
+
```
|
|
238
|
+
|
|
204
239
|
## Notes
|
|
205
240
|
|
|
206
241
|
- Yahoo downloads can be cached under `output/data` by default.
|
|
207
242
|
- The engine is intended for historical research, not brokerage execution.
|
|
208
243
|
- File output only happens through the reporting and cache helpers.
|
|
244
|
+
- CommonJS and ESM are both supported.
|
package/bin/tradelab.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
backtest,
|
|
7
|
+
backtestPortfolio,
|
|
8
|
+
ema,
|
|
9
|
+
exportBacktestArtifacts,
|
|
10
|
+
exportMetricsJSON,
|
|
11
|
+
getHistoricalCandles,
|
|
12
|
+
loadCandlesFromCSV,
|
|
13
|
+
saveCandlesToCache,
|
|
14
|
+
walkForwardOptimize,
|
|
15
|
+
} from "../src/index.js";
|
|
16
|
+
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const args = { _: [] };
|
|
19
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
20
|
+
const token = argv[index];
|
|
21
|
+
if (!token.startsWith("--")) {
|
|
22
|
+
args._.push(token);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const key = token.slice(2);
|
|
27
|
+
const next = argv[index + 1];
|
|
28
|
+
if (next && !next.startsWith("--")) {
|
|
29
|
+
args[key] = next;
|
|
30
|
+
index += 1;
|
|
31
|
+
} else {
|
|
32
|
+
args[key] = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return args;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toNumber(value, fallback) {
|
|
39
|
+
const numeric = Number(value);
|
|
40
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toList(value, fallback) {
|
|
44
|
+
if (!value) return fallback;
|
|
45
|
+
return String(value)
|
|
46
|
+
.split(",")
|
|
47
|
+
.map((item) => Number(item.trim()))
|
|
48
|
+
.filter((item) => Number.isFinite(item));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createEmaCrossSignal({
|
|
52
|
+
fast = 10,
|
|
53
|
+
slow = 30,
|
|
54
|
+
rr = 2,
|
|
55
|
+
stopLookback = 15,
|
|
56
|
+
} = {}) {
|
|
57
|
+
return ({ candles }) => {
|
|
58
|
+
if (candles.length < Math.max(fast, slow) + 2) return null;
|
|
59
|
+
|
|
60
|
+
const closes = candles.map((bar) => bar.close);
|
|
61
|
+
const fastLine = ema(closes, fast);
|
|
62
|
+
const slowLine = ema(closes, slow);
|
|
63
|
+
const last = closes.length - 1;
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
fastLine[last - 1] <= slowLine[last - 1] &&
|
|
67
|
+
fastLine[last] > slowLine[last]
|
|
68
|
+
) {
|
|
69
|
+
const entry = candles[last].close;
|
|
70
|
+
const stop = Math.min(
|
|
71
|
+
...candles.slice(-stopLookback).map((bar) => bar.low)
|
|
72
|
+
);
|
|
73
|
+
if (entry > stop) {
|
|
74
|
+
return { side: "long", entry, stop, rr };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
fastLine[last - 1] >= slowLine[last - 1] &&
|
|
80
|
+
fastLine[last] < slowLine[last]
|
|
81
|
+
) {
|
|
82
|
+
const entry = candles[last].close;
|
|
83
|
+
const stop = Math.max(
|
|
84
|
+
...candles.slice(-stopLookback).map((bar) => bar.high)
|
|
85
|
+
);
|
|
86
|
+
if (entry < stop) {
|
|
87
|
+
return { side: "short", entry, stop, rr };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createBuyHoldSignal({ holdBars = 5, stopPct = 0.05 } = {}) {
|
|
96
|
+
let entered = false;
|
|
97
|
+
|
|
98
|
+
return ({ bar }) => {
|
|
99
|
+
if (entered) return null;
|
|
100
|
+
entered = true;
|
|
101
|
+
return {
|
|
102
|
+
side: "long",
|
|
103
|
+
entry: bar.close,
|
|
104
|
+
stop: bar.close * (1 - stopPct),
|
|
105
|
+
rr: 100,
|
|
106
|
+
_maxBarsInTrade: holdBars,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function loadStrategy(strategyArg, args) {
|
|
112
|
+
if (!strategyArg || strategyArg === "ema-cross") {
|
|
113
|
+
return createEmaCrossSignal({
|
|
114
|
+
fast: toNumber(args.fast, 10),
|
|
115
|
+
slow: toNumber(args.slow, 30),
|
|
116
|
+
rr: toNumber(args.rr, 2),
|
|
117
|
+
stopLookback: toNumber(args.stopLookback, 15),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (strategyArg === "buy-hold") {
|
|
122
|
+
return createBuyHoldSignal({
|
|
123
|
+
holdBars: toNumber(args.holdBars, 5),
|
|
124
|
+
stopPct: toNumber(args.stopPct, 0.05),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const resolved = path.resolve(process.cwd(), strategyArg);
|
|
129
|
+
const module = await import(pathToFileURL(resolved).href);
|
|
130
|
+
if (typeof module.default === "function") return module.default(args);
|
|
131
|
+
if (typeof module.createSignal === "function") return module.createSignal(args);
|
|
132
|
+
if (typeof module.signal === "function") return module.signal;
|
|
133
|
+
throw new Error(`Strategy module "${strategyArg}" must export default, createSignal, or signal`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function printHelp() {
|
|
137
|
+
console.log(`tradelab
|
|
138
|
+
|
|
139
|
+
Commands:
|
|
140
|
+
backtest Run a one-off backtest from Yahoo or CSV data
|
|
141
|
+
portfolio Run multiple CSV datasets as an equal-weight portfolio
|
|
142
|
+
walk-forward Run rolling train/test optimization with the built-in ema-cross strategy
|
|
143
|
+
prefetch Download Yahoo candles into the local cache
|
|
144
|
+
import-csv Normalize a CSV and save it into the local cache
|
|
145
|
+
|
|
146
|
+
Examples:
|
|
147
|
+
tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
|
|
148
|
+
tradelab backtest --source csv --csvPath ./data/btc.csv --strategy buy-hold --holdBars 3
|
|
149
|
+
tradelab walk-forward --source csv --csvPath ./data/spy.csv --trainBars 120 --testBars 40
|
|
150
|
+
`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function commandBacktest(args) {
|
|
154
|
+
const candles = await getHistoricalCandles({
|
|
155
|
+
source: args.source || (args.csvPath ? "csv" : "yahoo"),
|
|
156
|
+
symbol: args.symbol,
|
|
157
|
+
interval: args.interval || "1d",
|
|
158
|
+
period: args.period || "1y",
|
|
159
|
+
csvPath: args.csvPath,
|
|
160
|
+
cache: args.cache !== "false",
|
|
161
|
+
});
|
|
162
|
+
const signal = await loadStrategy(args.strategy, args);
|
|
163
|
+
const result = backtest({
|
|
164
|
+
candles,
|
|
165
|
+
symbol: args.symbol || "DATA",
|
|
166
|
+
interval: args.interval || "1d",
|
|
167
|
+
range: args.period || "custom",
|
|
168
|
+
equity: toNumber(args.equity, 10_000),
|
|
169
|
+
riskPct: toNumber(args.riskPct, 1),
|
|
170
|
+
warmupBars: toNumber(args.warmupBars, 20),
|
|
171
|
+
flattenAtClose: args.flattenAtClose === true || args.flattenAtClose === "true",
|
|
172
|
+
signal,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const outputs = exportBacktestArtifacts({
|
|
176
|
+
result,
|
|
177
|
+
outDir: args.outDir || "output",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
console.log(
|
|
181
|
+
JSON.stringify(
|
|
182
|
+
{
|
|
183
|
+
symbol: result.symbol,
|
|
184
|
+
trades: result.metrics.trades,
|
|
185
|
+
winRate: result.metrics.winRate,
|
|
186
|
+
profitFactor: result.metrics.profitFactor,
|
|
187
|
+
finalEquity: result.metrics.finalEquity,
|
|
188
|
+
outputs,
|
|
189
|
+
},
|
|
190
|
+
null,
|
|
191
|
+
2
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parsePortfolioInputs(args) {
|
|
197
|
+
const csvPaths = String(args.csvPaths || "")
|
|
198
|
+
.split(",")
|
|
199
|
+
.map((value) => value.trim())
|
|
200
|
+
.filter(Boolean);
|
|
201
|
+
const symbols = String(args.symbols || "")
|
|
202
|
+
.split(",")
|
|
203
|
+
.map((value) => value.trim())
|
|
204
|
+
.filter(Boolean);
|
|
205
|
+
|
|
206
|
+
return csvPaths.map((csvPath, index) => ({
|
|
207
|
+
symbol: symbols[index] || `asset-${index + 1}`,
|
|
208
|
+
candles: loadCandlesFromCSV(csvPath),
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function commandPortfolio(args) {
|
|
213
|
+
const baseSystems = parsePortfolioInputs(args);
|
|
214
|
+
const systems = await Promise.all(
|
|
215
|
+
baseSystems.map(async (system) => ({
|
|
216
|
+
...system,
|
|
217
|
+
signal: await loadStrategy(args.strategy || "buy-hold", args),
|
|
218
|
+
warmupBars: toNumber(args.warmupBars, 1),
|
|
219
|
+
flattenAtClose: false,
|
|
220
|
+
}))
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const result = backtestPortfolio({
|
|
224
|
+
systems,
|
|
225
|
+
equity: toNumber(args.equity, 10_000),
|
|
226
|
+
collectReplay: false,
|
|
227
|
+
collectEqSeries: true,
|
|
228
|
+
});
|
|
229
|
+
const metricsPath = exportMetricsJSON({
|
|
230
|
+
result,
|
|
231
|
+
outDir: args.outDir || "output",
|
|
232
|
+
symbol: "PORTFOLIO",
|
|
233
|
+
interval: args.interval || "mixed",
|
|
234
|
+
range: args.period || "custom",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
console.log(
|
|
238
|
+
JSON.stringify(
|
|
239
|
+
{
|
|
240
|
+
systems: result.systems.length,
|
|
241
|
+
positions: result.positions.length,
|
|
242
|
+
finalEquity: result.metrics.finalEquity,
|
|
243
|
+
metricsPath,
|
|
244
|
+
},
|
|
245
|
+
null,
|
|
246
|
+
2
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function commandWalkForward(args) {
|
|
252
|
+
const candles = await getHistoricalCandles({
|
|
253
|
+
source: args.source || (args.csvPath ? "csv" : "yahoo"),
|
|
254
|
+
symbol: args.symbol,
|
|
255
|
+
interval: args.interval || "1d",
|
|
256
|
+
period: args.period || "1y",
|
|
257
|
+
csvPath: args.csvPath,
|
|
258
|
+
cache: args.cache !== "false",
|
|
259
|
+
});
|
|
260
|
+
const fasts = toList(args.fasts, [8, 10, 12]);
|
|
261
|
+
const slows = toList(args.slows, [20, 30, 40]);
|
|
262
|
+
const rrs = toList(args.rrs, [1.5, 2, 3]);
|
|
263
|
+
const parameterSets = [];
|
|
264
|
+
|
|
265
|
+
for (const fast of fasts) {
|
|
266
|
+
for (const slow of slows) {
|
|
267
|
+
if (fast >= slow) continue;
|
|
268
|
+
for (const rr of rrs) {
|
|
269
|
+
parameterSets.push({ fast, slow, rr });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = walkForwardOptimize({
|
|
275
|
+
candles,
|
|
276
|
+
parameterSets,
|
|
277
|
+
trainBars: toNumber(args.trainBars, 120),
|
|
278
|
+
testBars: toNumber(args.testBars, 40),
|
|
279
|
+
stepBars: toNumber(args.stepBars, toNumber(args.testBars, 40)),
|
|
280
|
+
scoreBy: args.scoreBy || "profitFactor",
|
|
281
|
+
backtestOptions: {
|
|
282
|
+
symbol: args.symbol || "DATA",
|
|
283
|
+
interval: args.interval || "1d",
|
|
284
|
+
range: args.period || "custom",
|
|
285
|
+
equity: toNumber(args.equity, 10_000),
|
|
286
|
+
riskPct: toNumber(args.riskPct, 1),
|
|
287
|
+
warmupBars: toNumber(args.warmupBars, 20),
|
|
288
|
+
},
|
|
289
|
+
signalFactory(params) {
|
|
290
|
+
return createEmaCrossSignal({
|
|
291
|
+
fast: params.fast,
|
|
292
|
+
slow: params.slow,
|
|
293
|
+
rr: params.rr,
|
|
294
|
+
stopLookback: toNumber(args.stopLookback, 15),
|
|
295
|
+
});
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const metricsPath = exportMetricsJSON({
|
|
300
|
+
result,
|
|
301
|
+
outDir: args.outDir || "output",
|
|
302
|
+
symbol: args.symbol || "DATA",
|
|
303
|
+
interval: args.interval || "1d",
|
|
304
|
+
range: `${args.trainBars || 120}-${args.testBars || 40}`,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
console.log(
|
|
308
|
+
JSON.stringify(
|
|
309
|
+
{
|
|
310
|
+
windows: result.windows.length,
|
|
311
|
+
positions: result.positions.length,
|
|
312
|
+
finalEquity: result.metrics.finalEquity,
|
|
313
|
+
metricsPath,
|
|
314
|
+
},
|
|
315
|
+
null,
|
|
316
|
+
2
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function commandPrefetch(args) {
|
|
322
|
+
const candles = await getHistoricalCandles({
|
|
323
|
+
source: "yahoo",
|
|
324
|
+
symbol: args.symbol || "SPY",
|
|
325
|
+
interval: args.interval || "1d",
|
|
326
|
+
period: args.period || "1y",
|
|
327
|
+
cache: false,
|
|
328
|
+
});
|
|
329
|
+
const outputPath = saveCandlesToCache(candles, {
|
|
330
|
+
symbol: args.symbol || "SPY",
|
|
331
|
+
interval: args.interval || "1d",
|
|
332
|
+
period: args.period || "1y",
|
|
333
|
+
outDir: args.outDir || "output/data",
|
|
334
|
+
source: "yahoo",
|
|
335
|
+
});
|
|
336
|
+
console.log(`Saved ${candles.length} candles to ${outputPath}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function commandImportCsv(args) {
|
|
340
|
+
const csvPath = args.csvPath || args._[1];
|
|
341
|
+
if (!csvPath) {
|
|
342
|
+
throw new Error("import-csv requires --csvPath or a positional CSV file path");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const candles = loadCandlesFromCSV(csvPath, {});
|
|
346
|
+
const outputPath = saveCandlesToCache(candles, {
|
|
347
|
+
symbol: args.symbol || "DATA",
|
|
348
|
+
interval: args.interval || "1d",
|
|
349
|
+
period: args.period || "custom",
|
|
350
|
+
outDir: args.outDir || "output/data",
|
|
351
|
+
source: "csv",
|
|
352
|
+
});
|
|
353
|
+
console.log(`Saved ${candles.length} candles to ${outputPath}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const commands = {
|
|
357
|
+
backtest: commandBacktest,
|
|
358
|
+
portfolio: commandPortfolio,
|
|
359
|
+
"walk-forward": commandWalkForward,
|
|
360
|
+
prefetch: commandPrefetch,
|
|
361
|
+
"import-csv": commandImportCsv,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
async function main() {
|
|
365
|
+
const args = parseArgs(process.argv.slice(2));
|
|
366
|
+
const command = args._[0];
|
|
367
|
+
|
|
368
|
+
if (!command || command === "help" || args.help) {
|
|
369
|
+
printHelp();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const handler = commands[command];
|
|
374
|
+
if (!handler) {
|
|
375
|
+
throw new Error(`Unknown command "${command}". Run "tradelab help".`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await handler(args);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
main().catch((error) => {
|
|
382
|
+
console.error(error.message);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
});
|