tradelab 0.4.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 +121 -52
- package/bin/tradelab.js +340 -49
- package/dist/cjs/data.cjs +210 -155
- package/dist/cjs/index.cjs +1782 -274
- package/dist/cjs/live.cjs +3350 -0
- package/docs/README.md +26 -9
- package/docs/api-reference.md +89 -26
- package/docs/backtest-engine.md +74 -60
- package/docs/data-reporting-cli.md +66 -36
- package/docs/examples.md +275 -0
- 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 +481 -0
- package/src/engine/barSystemRunner.js +1027 -0
- package/src/engine/execution.js +11 -39
- package/src/engine/portfolio.js +237 -66
- package/src/engine/walkForward.js +132 -13
- package/src/index.js +3 -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 +103 -100
- 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 +98 -4
- package/types/live.d.ts +382 -0
package/bin/tradelab.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import { pathToFileURL } from "url";
|
|
4
5
|
|
|
@@ -13,6 +14,16 @@ import {
|
|
|
13
14
|
saveCandlesToCache,
|
|
14
15
|
walkForwardOptimize,
|
|
15
16
|
} from "../src/index.js";
|
|
17
|
+
import {
|
|
18
|
+
AlpacaBroker,
|
|
19
|
+
BinanceBroker,
|
|
20
|
+
CoinbaseBroker,
|
|
21
|
+
InteractiveBrokersBroker,
|
|
22
|
+
JsonFileStorage,
|
|
23
|
+
LiveEngine,
|
|
24
|
+
LiveOrchestrator,
|
|
25
|
+
PaperEngine,
|
|
26
|
+
} from "../src/live/index.js";
|
|
16
27
|
|
|
17
28
|
function parseArgs(argv) {
|
|
18
29
|
const args = { _: [] };
|
|
@@ -24,12 +35,15 @@ function parseArgs(argv) {
|
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
const key = token.slice(2);
|
|
38
|
+
const camelKey = key.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
27
39
|
const next = argv[index + 1];
|
|
40
|
+
const value = next && !next.startsWith("--") ? next : true;
|
|
41
|
+
args[key] = value;
|
|
42
|
+
if (camelKey !== key && args[camelKey] === undefined) {
|
|
43
|
+
args[camelKey] = value;
|
|
44
|
+
}
|
|
28
45
|
if (next && !next.startsWith("--")) {
|
|
29
|
-
args[key] = next;
|
|
30
46
|
index += 1;
|
|
31
|
-
} else {
|
|
32
|
-
args[key] = true;
|
|
33
47
|
}
|
|
34
48
|
}
|
|
35
49
|
return args;
|
|
@@ -48,12 +62,73 @@ function toList(value, fallback) {
|
|
|
48
62
|
.filter((item) => Number.isFinite(item));
|
|
49
63
|
}
|
|
50
64
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
function parseJsonValue(value, fallback = null) {
|
|
66
|
+
if (!value) return fallback;
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(String(value));
|
|
69
|
+
} catch {
|
|
70
|
+
throw new Error(`Invalid JSON value: ${String(value).slice(0, 120)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toBoolean(value, fallback = false) {
|
|
75
|
+
if (value === undefined || value === null) return fallback;
|
|
76
|
+
if (value === true || value === false) return value;
|
|
77
|
+
const normalized = String(value).trim().toLowerCase();
|
|
78
|
+
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|
79
|
+
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function loadJsonFile(filePath) {
|
|
84
|
+
const resolved = path.resolve(process.cwd(), filePath);
|
|
85
|
+
const raw = fs.readFileSync(resolved, "utf8");
|
|
86
|
+
return JSON.parse(raw);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveBrokerName(name, paperMode = false) {
|
|
90
|
+
if (paperMode) return "paper";
|
|
91
|
+
return String(name || "paper").toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createBrokerAdapter(args, overrides = {}) {
|
|
95
|
+
const brokerName = resolveBrokerName(
|
|
96
|
+
overrides.broker || args.broker,
|
|
97
|
+
toBoolean(overrides.paper ?? args.paper, false)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (brokerName === "paper") {
|
|
101
|
+
return new PaperEngine({
|
|
102
|
+
equity: toNumber(overrides.equity ?? args.equity, 10_000),
|
|
103
|
+
slippageBps: toNumber(overrides.slippageBps ?? args.slippageBps, 0),
|
|
104
|
+
feeBps: toNumber(overrides.feeBps ?? args.feeBps, 0),
|
|
105
|
+
costs: parseJsonValue(overrides.costs ?? args.costs, null),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (brokerName === "alpaca") return new AlpacaBroker();
|
|
110
|
+
if (brokerName === "binance") return new BinanceBroker();
|
|
111
|
+
if (brokerName === "coinbase") return new CoinbaseBroker();
|
|
112
|
+
if (brokerName === "ib" || brokerName === "interactivebrokers") {
|
|
113
|
+
return new InteractiveBrokersBroker();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new Error(`Unsupported broker "${brokerName}"`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function brokerConfigFromArgs(args, overrides = {}) {
|
|
120
|
+
return {
|
|
121
|
+
apiKey: overrides.apiKey ?? args.apiKey,
|
|
122
|
+
apiSecret: overrides.apiSecret ?? args.apiSecret,
|
|
123
|
+
passphrase: overrides.passphrase ?? args.passphrase,
|
|
124
|
+
paper: toBoolean(overrides.paper ?? args.paper, false),
|
|
125
|
+
baseUrl: overrides.baseUrl ?? args.baseUrl,
|
|
126
|
+
wsUrl: overrides.wsUrl ?? args.wsUrl,
|
|
127
|
+
futures: toBoolean(overrides.futures ?? args.futures, false),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createEmaCrossSignal({ fast = 10, slow = 30, rr = 2, stopLookback = 15 } = {}) {
|
|
57
132
|
return ({ candles }) => {
|
|
58
133
|
if (candles.length < Math.max(fast, slow) + 2) return null;
|
|
59
134
|
|
|
@@ -62,27 +137,17 @@ function createEmaCrossSignal({
|
|
|
62
137
|
const slowLine = ema(closes, slow);
|
|
63
138
|
const last = closes.length - 1;
|
|
64
139
|
|
|
65
|
-
if (
|
|
66
|
-
fastLine[last - 1] <= slowLine[last - 1] &&
|
|
67
|
-
fastLine[last] > slowLine[last]
|
|
68
|
-
) {
|
|
140
|
+
if (fastLine[last - 1] <= slowLine[last - 1] && fastLine[last] > slowLine[last]) {
|
|
69
141
|
const entry = candles[last].close;
|
|
70
|
-
const stop = Math.min(
|
|
71
|
-
...candles.slice(-stopLookback).map((bar) => bar.low)
|
|
72
|
-
);
|
|
142
|
+
const stop = Math.min(...candles.slice(-stopLookback).map((bar) => bar.low));
|
|
73
143
|
if (entry > stop) {
|
|
74
144
|
return { side: "long", entry, stop, rr };
|
|
75
145
|
}
|
|
76
146
|
}
|
|
77
147
|
|
|
78
|
-
if (
|
|
79
|
-
fastLine[last - 1] >= slowLine[last - 1] &&
|
|
80
|
-
fastLine[last] < slowLine[last]
|
|
81
|
-
) {
|
|
148
|
+
if (fastLine[last - 1] >= slowLine[last - 1] && fastLine[last] < slowLine[last]) {
|
|
82
149
|
const entry = candles[last].close;
|
|
83
|
-
const stop = Math.max(
|
|
84
|
-
...candles.slice(-stopLookback).map((bar) => bar.high)
|
|
85
|
-
);
|
|
150
|
+
const stop = Math.max(...candles.slice(-stopLookback).map((bar) => bar.high));
|
|
86
151
|
if (entry < stop) {
|
|
87
152
|
return { side: "short", entry, stop, rr };
|
|
88
153
|
}
|
|
@@ -133,13 +198,73 @@ async function loadStrategy(strategyArg, args) {
|
|
|
133
198
|
throw new Error(`Strategy module "${strategyArg}" must export default, createSignal, or signal`);
|
|
134
199
|
}
|
|
135
200
|
|
|
201
|
+
async function loadWalkForwardStrategy(strategyArg, args) {
|
|
202
|
+
if (!strategyArg || strategyArg === "ema-cross") {
|
|
203
|
+
const fasts = toList(args.fasts, [8, 10, 12]);
|
|
204
|
+
const slows = toList(args.slows, [20, 30, 40]);
|
|
205
|
+
const rrs = toList(args.rrs, [1.5, 2, 3]);
|
|
206
|
+
const parameterSets = [];
|
|
207
|
+
|
|
208
|
+
for (const fast of fasts) {
|
|
209
|
+
for (const slow of slows) {
|
|
210
|
+
if (fast >= slow) continue;
|
|
211
|
+
for (const rr of rrs) {
|
|
212
|
+
parameterSets.push({ fast, slow, rr });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
parameterSets,
|
|
219
|
+
signalFactory(params) {
|
|
220
|
+
return createEmaCrossSignal({
|
|
221
|
+
fast: params.fast,
|
|
222
|
+
slow: params.slow,
|
|
223
|
+
rr: params.rr,
|
|
224
|
+
stopLookback: toNumber(args.stopLookback, 15),
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const resolved = path.resolve(process.cwd(), strategyArg);
|
|
231
|
+
const module = await import(pathToFileURL(resolved).href);
|
|
232
|
+
if (typeof module.signalFactory !== "function") {
|
|
233
|
+
throw new Error(`Walk-forward strategy module "${strategyArg}" must export signalFactory`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const parameterSets =
|
|
237
|
+
parseJsonValue(args.parameterSets) ??
|
|
238
|
+
(typeof module.createParameterSets === "function"
|
|
239
|
+
? await module.createParameterSets(args)
|
|
240
|
+
: module.parameterSets);
|
|
241
|
+
|
|
242
|
+
if (!Array.isArray(parameterSets) || parameterSets.length === 0) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Walk-forward strategy module "${strategyArg}" must provide parameterSets, createParameterSets(args), or --parameterSets`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
parameterSets,
|
|
250
|
+
signalFactory(params) {
|
|
251
|
+
return module.signalFactory(params, args);
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
136
256
|
function printHelp() {
|
|
137
|
-
console.log(`tradelab
|
|
257
|
+
console.log(`tradelab — backtesting toolkit for Node.js
|
|
258
|
+
|
|
259
|
+
Usage: tradelab <command> [options]
|
|
138
260
|
|
|
139
261
|
Commands:
|
|
140
262
|
backtest Run a one-off backtest from Yahoo or CSV data
|
|
141
263
|
portfolio Run multiple CSV datasets as an equal-weight portfolio
|
|
142
|
-
walk-forward Run rolling train/test optimization
|
|
264
|
+
walk-forward Run rolling or anchored train/test optimization
|
|
265
|
+
live Run live trading engine (streaming or polling)
|
|
266
|
+
paper Run live engine in paper broker mode
|
|
267
|
+
status Read persisted live state
|
|
143
268
|
prefetch Download Yahoo candles into the local cache
|
|
144
269
|
import-csv Normalize a CSV and save it into the local cache
|
|
145
270
|
|
|
@@ -147,12 +272,21 @@ Examples:
|
|
|
147
272
|
tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
|
|
148
273
|
tradelab backtest --source csv --csvPath ./data/btc.csv --strategy buy-hold --holdBars 3
|
|
149
274
|
tradelab walk-forward --source csv --csvPath ./data/spy.csv --trainBars 120 --testBars 40
|
|
275
|
+
tradelab live --strategy ./mySignal.js --symbol AAPL --interval 5m --broker alpaca --paper
|
|
276
|
+
|
|
277
|
+
Options:
|
|
278
|
+
--help Show this help message
|
|
279
|
+
--version Print version number
|
|
150
280
|
`);
|
|
151
281
|
}
|
|
152
282
|
|
|
153
283
|
async function commandBacktest(args) {
|
|
284
|
+
const source = args.source || (args.csvPath ? "csv" : "yahoo");
|
|
285
|
+
if (source === "yahoo" && !args.symbol) {
|
|
286
|
+
throw new Error("backtest with Yahoo source requires --symbol (e.g. --symbol SPY)");
|
|
287
|
+
}
|
|
154
288
|
const candles = await getHistoricalCandles({
|
|
155
|
-
source
|
|
289
|
+
source,
|
|
156
290
|
symbol: args.symbol,
|
|
157
291
|
interval: args.interval || "1d",
|
|
158
292
|
period: args.period || "1y",
|
|
@@ -249,34 +383,27 @@ async function commandPortfolio(args) {
|
|
|
249
383
|
}
|
|
250
384
|
|
|
251
385
|
async function commandWalkForward(args) {
|
|
386
|
+
const wfSource = args.source || (args.csvPath ? "csv" : "yahoo");
|
|
387
|
+
if (wfSource === "yahoo" && !args.symbol) {
|
|
388
|
+
throw new Error("walk-forward with Yahoo source requires --symbol (e.g. --symbol QQQ)");
|
|
389
|
+
}
|
|
252
390
|
const candles = await getHistoricalCandles({
|
|
253
|
-
source:
|
|
391
|
+
source: wfSource,
|
|
254
392
|
symbol: args.symbol,
|
|
255
393
|
interval: args.interval || "1d",
|
|
256
394
|
period: args.period || "1y",
|
|
257
395
|
csvPath: args.csvPath,
|
|
258
396
|
cache: args.cache !== "false",
|
|
259
397
|
});
|
|
260
|
-
const
|
|
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
|
-
}
|
|
398
|
+
const walkForwardStrategy = await loadWalkForwardStrategy(args.strategy, args);
|
|
273
399
|
|
|
274
400
|
const result = walkForwardOptimize({
|
|
275
401
|
candles,
|
|
276
|
-
parameterSets,
|
|
402
|
+
parameterSets: walkForwardStrategy.parameterSets,
|
|
277
403
|
trainBars: toNumber(args.trainBars, 120),
|
|
278
404
|
testBars: toNumber(args.testBars, 40),
|
|
279
405
|
stepBars: toNumber(args.stepBars, toNumber(args.testBars, 40)),
|
|
406
|
+
mode: args.mode || "rolling",
|
|
280
407
|
scoreBy: args.scoreBy || "profitFactor",
|
|
281
408
|
backtestOptions: {
|
|
282
409
|
symbol: args.symbol || "DATA",
|
|
@@ -286,14 +413,7 @@ async function commandWalkForward(args) {
|
|
|
286
413
|
riskPct: toNumber(args.riskPct, 1),
|
|
287
414
|
warmupBars: toNumber(args.warmupBars, 20),
|
|
288
415
|
},
|
|
289
|
-
signalFactory
|
|
290
|
-
return createEmaCrossSignal({
|
|
291
|
-
fast: params.fast,
|
|
292
|
-
slow: params.slow,
|
|
293
|
-
rr: params.rr,
|
|
294
|
-
stopLookback: toNumber(args.stopLookback, 15),
|
|
295
|
-
});
|
|
296
|
-
},
|
|
416
|
+
signalFactory: walkForwardStrategy.signalFactory,
|
|
297
417
|
});
|
|
298
418
|
|
|
299
419
|
const metricsPath = exportMetricsJSON({
|
|
@@ -310,6 +430,7 @@ async function commandWalkForward(args) {
|
|
|
310
430
|
windows: result.windows.length,
|
|
311
431
|
positions: result.positions.length,
|
|
312
432
|
finalEquity: result.metrics.finalEquity,
|
|
433
|
+
bestParamsSummary: result.bestParamsSummary,
|
|
313
434
|
metricsPath,
|
|
314
435
|
},
|
|
315
436
|
null,
|
|
@@ -353,10 +474,174 @@ async function commandImportCsv(args) {
|
|
|
353
474
|
console.log(`Saved ${candles.length} candles to ${outputPath}`);
|
|
354
475
|
}
|
|
355
476
|
|
|
477
|
+
async function createLiveSystemFromConfig(system, args) {
|
|
478
|
+
const signal = await loadStrategy(system.strategy || args.strategy, {
|
|
479
|
+
...args,
|
|
480
|
+
...system,
|
|
481
|
+
});
|
|
482
|
+
return {
|
|
483
|
+
...system,
|
|
484
|
+
signal,
|
|
485
|
+
interval: system.interval || args.interval || "1m",
|
|
486
|
+
symbol: system.symbol || args.symbol,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function commandLive(args, overrides = {}) {
|
|
491
|
+
const configPath = overrides.config || args.config;
|
|
492
|
+
const mode = overrides.mode || args.mode || "streaming";
|
|
493
|
+
const stateDir = overrides.stateDir || args.stateDir || "output/live-state";
|
|
494
|
+
const once = toBoolean(overrides.once ?? args.once, mode === "polling");
|
|
495
|
+
const watch = toBoolean(overrides.watch ?? args.watch, false);
|
|
496
|
+
const storage = new JsonFileStorage({ baseDir: stateDir });
|
|
497
|
+
|
|
498
|
+
if (configPath) {
|
|
499
|
+
const fileConfig = loadJsonFile(configPath);
|
|
500
|
+
const broker = createBrokerAdapter(args, {
|
|
501
|
+
...overrides,
|
|
502
|
+
equity: fileConfig.equity ?? overrides.equity,
|
|
503
|
+
});
|
|
504
|
+
const brokerConfig = brokerConfigFromArgs(args, overrides);
|
|
505
|
+
const systems = await Promise.all(
|
|
506
|
+
(fileConfig.systems || []).map((system) => createLiveSystemFromConfig(system, args))
|
|
507
|
+
);
|
|
508
|
+
const orchestrator = new LiveOrchestrator({
|
|
509
|
+
systems,
|
|
510
|
+
broker,
|
|
511
|
+
storage,
|
|
512
|
+
brokerConfig,
|
|
513
|
+
allocation: fileConfig.allocation || args.allocation || "equal",
|
|
514
|
+
maxDailyLossPct: toNumber(fileConfig.maxDailyLossPct ?? args.maxDailyLossPct, 0),
|
|
515
|
+
equity: toNumber(fileConfig.equity ?? args.equity, 10_000),
|
|
516
|
+
});
|
|
517
|
+
await broker.connect(brokerConfig);
|
|
518
|
+
await orchestrator.start();
|
|
519
|
+
|
|
520
|
+
if (once && orchestrator.engines?.length) {
|
|
521
|
+
await Promise.all(orchestrator.engines.map((engine) => engine.pollOnce()));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const status = orchestrator.getStatus();
|
|
525
|
+
console.log(JSON.stringify(status, null, 2));
|
|
526
|
+
|
|
527
|
+
if (!watch) {
|
|
528
|
+
await orchestrator.stop();
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const broker = createBrokerAdapter(args, overrides);
|
|
534
|
+
const brokerConfig = brokerConfigFromArgs(args, overrides);
|
|
535
|
+
const signal = await loadStrategy(overrides.strategy || args.strategy, args);
|
|
536
|
+
const engine = new LiveEngine({
|
|
537
|
+
id: overrides.id || args.id,
|
|
538
|
+
signal,
|
|
539
|
+
symbol: overrides.symbol || args.symbol,
|
|
540
|
+
interval: overrides.interval || args.interval || "1m",
|
|
541
|
+
mode,
|
|
542
|
+
pollIntervalMs: toNumber(overrides.pollIntervalMs ?? args.pollIntervalMs, 60_000),
|
|
543
|
+
warmupBars: toNumber(overrides.warmupBars ?? args.warmupBars, 200),
|
|
544
|
+
equity: toNumber(overrides.equity ?? args.equity, 10_000),
|
|
545
|
+
riskPct: toNumber(overrides.riskPct ?? args.riskPct, 1),
|
|
546
|
+
costs: parseJsonValue(overrides.costs ?? args.costs, null),
|
|
547
|
+
flattenAtClose: toBoolean(overrides.flattenAtClose ?? args.flattenAtClose, false),
|
|
548
|
+
maxDailyLossPct: toNumber(overrides.maxDailyLossPct ?? args.maxDailyLossPct, 0),
|
|
549
|
+
dailyMaxTrades: toNumber(overrides.dailyMaxTrades ?? args.dailyMaxTrades, 0),
|
|
550
|
+
broker,
|
|
551
|
+
storage,
|
|
552
|
+
brokerConfig,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
await engine.start();
|
|
556
|
+
|
|
557
|
+
if (once) {
|
|
558
|
+
await engine.pollOnce();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const status = engine.getStatus();
|
|
562
|
+
console.log(JSON.stringify(status, null, 2));
|
|
563
|
+
|
|
564
|
+
if (!watch) {
|
|
565
|
+
await engine.stop();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const shutdown = async () => {
|
|
570
|
+
await engine.stop();
|
|
571
|
+
process.exit(0);
|
|
572
|
+
};
|
|
573
|
+
process.once("SIGINT", shutdown);
|
|
574
|
+
process.once("SIGTERM", shutdown);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function commandPaper(args) {
|
|
578
|
+
return commandLive(args, { paper: true, broker: "paper" });
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function commandStatus(args) {
|
|
582
|
+
const stateDir = args.dir || args.stateDir || "output/live-state";
|
|
583
|
+
const storage = new JsonFileStorage({ baseDir: stateDir });
|
|
584
|
+
const namespace = args.namespace || args.id;
|
|
585
|
+
|
|
586
|
+
if (namespace) {
|
|
587
|
+
const state = await storage.load(namespace);
|
|
588
|
+
const trades = await storage.loadTrades(namespace);
|
|
589
|
+
const equity = await storage.loadEquityCurve(namespace);
|
|
590
|
+
console.log(
|
|
591
|
+
JSON.stringify(
|
|
592
|
+
{
|
|
593
|
+
namespace,
|
|
594
|
+
state,
|
|
595
|
+
trades: trades.length,
|
|
596
|
+
equityPoints: equity.length,
|
|
597
|
+
},
|
|
598
|
+
null,
|
|
599
|
+
2
|
|
600
|
+
)
|
|
601
|
+
);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!fs.existsSync(stateDir)) {
|
|
606
|
+
console.log(JSON.stringify({ dir: stateDir, namespaces: [] }, null, 2));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const namespaces = fs
|
|
611
|
+
.readdirSync(stateDir, { withFileTypes: true })
|
|
612
|
+
.filter((entry) => entry.isDirectory())
|
|
613
|
+
.map((entry) => entry.name);
|
|
614
|
+
const summaries = [];
|
|
615
|
+
for (const name of namespaces) {
|
|
616
|
+
const state = await storage.load(name);
|
|
617
|
+
const trades = await storage.loadTrades(name);
|
|
618
|
+
summaries.push({
|
|
619
|
+
namespace: name,
|
|
620
|
+
savedAt: state?.savedAt ?? null,
|
|
621
|
+
equity: state?.equity ?? null,
|
|
622
|
+
openPosition: Boolean(state?.openPosition),
|
|
623
|
+
trades: trades.length,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
console.log(
|
|
627
|
+
JSON.stringify(
|
|
628
|
+
{
|
|
629
|
+
dir: stateDir,
|
|
630
|
+
namespaces: summaries,
|
|
631
|
+
},
|
|
632
|
+
null,
|
|
633
|
+
2
|
|
634
|
+
)
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
356
638
|
const commands = {
|
|
357
639
|
backtest: commandBacktest,
|
|
358
640
|
portfolio: commandPortfolio,
|
|
359
641
|
"walk-forward": commandWalkForward,
|
|
642
|
+
live: commandLive,
|
|
643
|
+
paper: commandPaper,
|
|
644
|
+
status: commandStatus,
|
|
360
645
|
prefetch: commandPrefetch,
|
|
361
646
|
"import-csv": commandImportCsv,
|
|
362
647
|
};
|
|
@@ -365,6 +650,12 @@ async function main() {
|
|
|
365
650
|
const args = parseArgs(process.argv.slice(2));
|
|
366
651
|
const command = args._[0];
|
|
367
652
|
|
|
653
|
+
if (args.version || args.v) {
|
|
654
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
655
|
+
console.log(pkg.version);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
368
659
|
if (!command || command === "help" || args.help) {
|
|
369
660
|
printHelp();
|
|
370
661
|
return;
|