@backtest-kit/cli 5.11.1 → 6.0.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/build/index.cjs +178 -17
- package/build/index.mjs +178 -17
- package/package.json +13 -13
- package/template/project/README.md +207 -0
- package/template/project/package.mustache +5 -5
- package/template/project/scripts/fetch_docs.mjs +19 -10
package/build/index.cjs
CHANGED
|
@@ -557,6 +557,14 @@ const getArgs = functoolsKit.singleshot(() => {
|
|
|
557
557
|
type: "boolean",
|
|
558
558
|
default: false,
|
|
559
559
|
},
|
|
560
|
+
help: {
|
|
561
|
+
type: "boolean",
|
|
562
|
+
default: false,
|
|
563
|
+
},
|
|
564
|
+
version: {
|
|
565
|
+
type: "boolean",
|
|
566
|
+
default: false,
|
|
567
|
+
},
|
|
560
568
|
},
|
|
561
569
|
strict: false,
|
|
562
570
|
allowPositionals: true,
|
|
@@ -2231,7 +2239,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
|
2231
2239
|
const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
|
|
2232
2240
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
2233
2241
|
});
|
|
2234
|
-
const main$
|
|
2242
|
+
const main$9 = async () => {
|
|
2235
2243
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
2236
2244
|
return;
|
|
2237
2245
|
}
|
|
@@ -2242,7 +2250,7 @@ const main$7 = async () => {
|
|
|
2242
2250
|
await cli.backtestMainService.connect();
|
|
2243
2251
|
listenGracefulShutdown$4();
|
|
2244
2252
|
};
|
|
2245
|
-
main$
|
|
2253
|
+
main$9();
|
|
2246
2254
|
|
|
2247
2255
|
const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
2248
2256
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -2263,7 +2271,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
|
2263
2271
|
const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
|
|
2264
2272
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
2265
2273
|
});
|
|
2266
|
-
const main$
|
|
2274
|
+
const main$8 = async () => {
|
|
2267
2275
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
2268
2276
|
return;
|
|
2269
2277
|
}
|
|
@@ -2274,7 +2282,7 @@ const main$6 = async () => {
|
|
|
2274
2282
|
cli.paperMainService.connect();
|
|
2275
2283
|
listenGracefulShutdown$3();
|
|
2276
2284
|
};
|
|
2277
|
-
main$
|
|
2285
|
+
main$8();
|
|
2278
2286
|
|
|
2279
2287
|
const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
|
|
2280
2288
|
process.off("SIGINT", BEFORE_EXIT_FN$2);
|
|
@@ -2295,7 +2303,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
|
|
|
2295
2303
|
const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
|
|
2296
2304
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
2297
2305
|
});
|
|
2298
|
-
const main$
|
|
2306
|
+
const main$7 = async () => {
|
|
2299
2307
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
2300
2308
|
return;
|
|
2301
2309
|
}
|
|
@@ -2306,7 +2314,7 @@ const main$5 = async () => {
|
|
|
2306
2314
|
await cli.liveMainService.connect();
|
|
2307
2315
|
listenGracefulShutdown$2();
|
|
2308
2316
|
};
|
|
2309
|
-
main$
|
|
2317
|
+
main$7();
|
|
2310
2318
|
|
|
2311
2319
|
const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
2312
2320
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -2316,7 +2324,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
|
2316
2324
|
const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
|
|
2317
2325
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
2318
2326
|
});
|
|
2319
|
-
const main$
|
|
2327
|
+
const main$6 = async () => {
|
|
2320
2328
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
2321
2329
|
return;
|
|
2322
2330
|
}
|
|
@@ -2326,7 +2334,7 @@ const main$4 = async () => {
|
|
|
2326
2334
|
}
|
|
2327
2335
|
listenGracefulShutdown$1();
|
|
2328
2336
|
};
|
|
2329
|
-
main$
|
|
2337
|
+
main$6();
|
|
2330
2338
|
|
|
2331
2339
|
const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
2332
2340
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -2336,7 +2344,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
|
2336
2344
|
const listenGracefulShutdown = functoolsKit.singleshot(() => {
|
|
2337
2345
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
2338
2346
|
});
|
|
2339
|
-
const main$
|
|
2347
|
+
const main$5 = async () => {
|
|
2340
2348
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
2341
2349
|
return;
|
|
2342
2350
|
}
|
|
@@ -2346,7 +2354,7 @@ const main$3 = async () => {
|
|
|
2346
2354
|
}
|
|
2347
2355
|
listenGracefulShutdown();
|
|
2348
2356
|
};
|
|
2349
|
-
main$
|
|
2357
|
+
main$5();
|
|
2350
2358
|
|
|
2351
2359
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
2352
2360
|
const keys = Object.keys(schema);
|
|
@@ -2368,7 +2376,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
2368
2376
|
}
|
|
2369
2377
|
return rows;
|
|
2370
2378
|
};
|
|
2371
|
-
const main$
|
|
2379
|
+
const main$4 = async () => {
|
|
2372
2380
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
2373
2381
|
return;
|
|
2374
2382
|
}
|
|
@@ -2421,6 +2429,7 @@ const main$2 = async () => {
|
|
|
2421
2429
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
2422
2430
|
await fs$1.writeFile(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
2423
2431
|
console.log(`Saved: ${filePath}`);
|
|
2432
|
+
process.exit(0);
|
|
2424
2433
|
return;
|
|
2425
2434
|
}
|
|
2426
2435
|
if (values.jsonl) {
|
|
@@ -2429,6 +2438,7 @@ const main$2 = async () => {
|
|
|
2429
2438
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
2430
2439
|
await fs$1.writeFile(filePath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
2431
2440
|
console.log(`Saved: ${filePath}`);
|
|
2441
|
+
process.exit(0);
|
|
2432
2442
|
return;
|
|
2433
2443
|
}
|
|
2434
2444
|
if (values.markdown) {
|
|
@@ -2436,13 +2446,15 @@ const main$2 = async () => {
|
|
|
2436
2446
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
2437
2447
|
await fs$1.writeFile(filePath, await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema), "utf-8");
|
|
2438
2448
|
console.log(`Saved: ${filePath}`);
|
|
2449
|
+
process.exit(0);
|
|
2439
2450
|
return;
|
|
2440
2451
|
}
|
|
2441
2452
|
console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
|
|
2453
|
+
process.exit(0);
|
|
2442
2454
|
};
|
|
2443
|
-
main$
|
|
2455
|
+
main$4();
|
|
2444
2456
|
|
|
2445
|
-
const main$
|
|
2457
|
+
const main$3 = async () => {
|
|
2446
2458
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
2447
2459
|
return;
|
|
2448
2460
|
}
|
|
@@ -2476,6 +2488,7 @@ const main$1 = async () => {
|
|
|
2476
2488
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
2477
2489
|
await fs$1.writeFile(filePath, JSON.stringify(candles, null, 2), "utf-8");
|
|
2478
2490
|
console.log(`Saved: ${filePath}`);
|
|
2491
|
+
process.exit(0);
|
|
2479
2492
|
return;
|
|
2480
2493
|
}
|
|
2481
2494
|
if (values.jsonl) {
|
|
@@ -2483,15 +2496,34 @@ const main$1 = async () => {
|
|
|
2483
2496
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
2484
2497
|
await fs$1.writeFile(filePath, candles.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
2485
2498
|
console.log(`Saved: ${filePath}`);
|
|
2499
|
+
process.exit(0);
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
if (values.markdown) {
|
|
2503
|
+
const header = `| open | high | low | close | volume | timestamp |\n| --- | --- | --- | --- | --- | --- |`;
|
|
2504
|
+
const rows = candles.map((c) => `| ${c.open} | ${c.high} | ${c.low} | ${c.close} | ${c.volume} | ${new Date(c.timestamp).toISOString()} |`);
|
|
2505
|
+
const md = [header, ...rows].join("\n");
|
|
2506
|
+
{
|
|
2507
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.md`);
|
|
2508
|
+
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
2509
|
+
await fs$1.writeFile(filePath, md, "utf-8");
|
|
2510
|
+
console.log(`Saved: ${filePath}`);
|
|
2511
|
+
}
|
|
2512
|
+
process.exit(0);
|
|
2486
2513
|
return;
|
|
2487
2514
|
}
|
|
2488
2515
|
console.log(JSON.stringify(candles, null, 2));
|
|
2516
|
+
process.exit(0);
|
|
2489
2517
|
};
|
|
2490
|
-
main$
|
|
2518
|
+
main$3();
|
|
2491
2519
|
|
|
2492
2520
|
const __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
2493
2521
|
const __dirname$1 = path.dirname(__filename$1);
|
|
2494
2522
|
const MUSTACHE_EXT = ".mustache";
|
|
2523
|
+
const MUSTACHE_RENAME = {
|
|
2524
|
+
"gitignore": ".gitignore",
|
|
2525
|
+
"package": "package.json",
|
|
2526
|
+
};
|
|
2495
2527
|
async function isDirEmpty(dirPath) {
|
|
2496
2528
|
try {
|
|
2497
2529
|
const files = await fs$1.readdir(dirPath);
|
|
@@ -2513,8 +2545,12 @@ async function copyDir(srcDir, destDir, data) {
|
|
|
2513
2545
|
await copyDir(srcPath, path.join(destDir, entry.name), data);
|
|
2514
2546
|
continue;
|
|
2515
2547
|
}
|
|
2548
|
+
if (entry.name === ".gitkeep") {
|
|
2549
|
+
continue;
|
|
2550
|
+
}
|
|
2516
2551
|
if (entry.name.endsWith(MUSTACHE_EXT)) {
|
|
2517
|
-
const
|
|
2552
|
+
const baseName = entry.name.slice(0, -MUSTACHE_EXT.length);
|
|
2553
|
+
const destName = MUSTACHE_RENAME[baseName] ?? baseName;
|
|
2518
2554
|
const destPath = path.join(destDir, destName);
|
|
2519
2555
|
const template = await fs$1.readFile(srcPath, "utf-8");
|
|
2520
2556
|
const rendered = Mustache.render(template, data);
|
|
@@ -2542,7 +2578,7 @@ function runScript(scriptPath, cwd) {
|
|
|
2542
2578
|
child.on("error", reject);
|
|
2543
2579
|
});
|
|
2544
2580
|
}
|
|
2545
|
-
const main = async () => {
|
|
2581
|
+
const main$2 = async () => {
|
|
2546
2582
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
2547
2583
|
return;
|
|
2548
2584
|
}
|
|
@@ -2552,7 +2588,7 @@ const main = async () => {
|
|
|
2552
2588
|
}
|
|
2553
2589
|
const projectName = values.output || "backtest-kit-project";
|
|
2554
2590
|
const projectPath = path.join(process.cwd(), projectName);
|
|
2555
|
-
const templatePath = path.join(__dirname$1, "
|
|
2591
|
+
const templatePath = path.join(__dirname$1, "../template/project");
|
|
2556
2592
|
const isEmpty = await isDirEmpty(projectPath);
|
|
2557
2593
|
if (!isEmpty) {
|
|
2558
2594
|
console.error(`Directory "${projectName}" already exists and is not empty.`);
|
|
@@ -2563,6 +2599,131 @@ const main = async () => {
|
|
|
2563
2599
|
console.log(`Fetching docs...`);
|
|
2564
2600
|
await runScript(path.join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
|
|
2565
2601
|
console.log(`Done! Project created at ${projectPath}`);
|
|
2602
|
+
process.exit(0);
|
|
2603
|
+
};
|
|
2604
|
+
main$2();
|
|
2605
|
+
|
|
2606
|
+
const HELP_TEXT = `
|
|
2607
|
+
@backtest-kit/cli
|
|
2608
|
+
|
|
2609
|
+
Usage:
|
|
2610
|
+
node index.mjs --<mode> [flags] [entry-point]
|
|
2611
|
+
|
|
2612
|
+
Modes:
|
|
2613
|
+
|
|
2614
|
+
--backtest <entry> Run strategy against historical candle data
|
|
2615
|
+
--paper <entry> Paper trading (live prices, no real orders)
|
|
2616
|
+
--live <entry> Live trading with real orders
|
|
2617
|
+
--pine <entry> Execute a local .pine indicator file
|
|
2618
|
+
--dump Fetch and save raw OHLCV candles
|
|
2619
|
+
--init Scaffold a new project in the current directory
|
|
2620
|
+
--help Print this help message
|
|
2621
|
+
|
|
2622
|
+
Backtest flags:
|
|
2623
|
+
|
|
2624
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2625
|
+
--strategy <string> Strategy name from addStrategySchema (default: first registered)
|
|
2626
|
+
--exchange <string> Exchange name from addExchangeSchema (default: first registered)
|
|
2627
|
+
--frame <string> Frame name from addFrameSchema (default: first registered)
|
|
2628
|
+
--cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
|
|
2629
|
+
--noCache Skip candle cache warming before the run
|
|
2630
|
+
--verbose Log every candle fetch to stdout
|
|
2631
|
+
--ui Start web dashboard at http://localhost:60050
|
|
2632
|
+
--telegram Send trade notifications to Telegram
|
|
2633
|
+
|
|
2634
|
+
Paper / Live flags:
|
|
2635
|
+
|
|
2636
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2637
|
+
--strategy <string> Strategy name (default: first registered)
|
|
2638
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2639
|
+
--verbose Log every candle fetch to stdout
|
|
2640
|
+
--ui Start web dashboard
|
|
2641
|
+
--telegram Send Telegram notifications
|
|
2642
|
+
|
|
2643
|
+
PineScript flags (--pine):
|
|
2644
|
+
|
|
2645
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2646
|
+
--timeframe <string> Candle interval (default: 15m)
|
|
2647
|
+
--limit <string> Number of candles to fetch (default: 250)
|
|
2648
|
+
--when <string> End date — ISO 8601 or Unix ms (default: now)
|
|
2649
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2650
|
+
--output <string> Output file base name without extension
|
|
2651
|
+
--json Save output as JSON array to <pine-dir>/dump/<output>.json
|
|
2652
|
+
--jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
|
|
2653
|
+
--markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
|
|
2654
|
+
|
|
2655
|
+
Only plot() calls with display=display.data_window produce output columns.
|
|
2656
|
+
Module file ./modules/pine.module is loaded automatically if it exists.
|
|
2657
|
+
|
|
2658
|
+
Candle dump flags (--dump):
|
|
2659
|
+
|
|
2660
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2661
|
+
--timeframe <string> Candle interval (default: 15m)
|
|
2662
|
+
--limit <string> Number of candles (default: 250)
|
|
2663
|
+
--when <string> End date — ISO 8601 or Unix ms (default: now)
|
|
2664
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2665
|
+
--output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
|
|
2666
|
+
--json Save as JSON array to ./dump/<output>.json
|
|
2667
|
+
--jsonl Save as JSONL to ./dump/<output>.jsonl
|
|
2668
|
+
|
|
2669
|
+
Module file ./modules/dump.module is loaded automatically if it exists.
|
|
2670
|
+
|
|
2671
|
+
Init flags (--init):
|
|
2672
|
+
|
|
2673
|
+
--output <string> Target directory name (default: backtest-kit-project)
|
|
2674
|
+
|
|
2675
|
+
Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
|
|
2676
|
+
|
|
2677
|
+
Module hooks (loaded automatically by each mode):
|
|
2678
|
+
|
|
2679
|
+
modules/backtest.module --backtest Broker adapter for backtest
|
|
2680
|
+
modules/paper.module --paper Broker adapter for paper trading
|
|
2681
|
+
modules/live.module --live Broker adapter for live trading
|
|
2682
|
+
modules/pine.module --pine Exchange schema for PineScript runs
|
|
2683
|
+
modules/dump.module --dump Exchange schema for candle dumps
|
|
2684
|
+
|
|
2685
|
+
Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
|
|
2686
|
+
|
|
2687
|
+
Environment variables:
|
|
2688
|
+
|
|
2689
|
+
CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
|
|
2690
|
+
CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
|
|
2691
|
+
CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
|
|
2692
|
+
CC_WWWROOT_PORT UI server port (default: 60050)
|
|
2693
|
+
|
|
2694
|
+
Examples:
|
|
2695
|
+
|
|
2696
|
+
node index.mjs --backtest ./content/feb_2026.strategy.ts
|
|
2697
|
+
node index.mjs --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
|
|
2698
|
+
node index.mjs --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
|
|
2699
|
+
node index.mjs --live --ui --telegram ./content/feb_2026.strategy.ts
|
|
2700
|
+
node index.mjs --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
|
|
2701
|
+
node index.mjs --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
|
|
2702
|
+
node index.mjs --init --output my-trading-bot
|
|
2703
|
+
`.trimStart();
|
|
2704
|
+
const main$1 = async () => {
|
|
2705
|
+
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
const { values } = getArgs();
|
|
2709
|
+
if (!values.help) {
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
process.stdout.write(HELP_TEXT);
|
|
2713
|
+
process.exit(0);
|
|
2714
|
+
};
|
|
2715
|
+
main$1();
|
|
2716
|
+
|
|
2717
|
+
const main = async () => {
|
|
2718
|
+
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
const { values } = getArgs();
|
|
2722
|
+
if (!values.version) {
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
process.stdout.write(`@backtest-kit/cli ${"6.0.0"}\n`);
|
|
2726
|
+
process.exit(0);
|
|
2566
2727
|
};
|
|
2567
2728
|
main();
|
|
2568
2729
|
|
package/build/index.mjs
CHANGED
|
@@ -532,6 +532,14 @@ const getArgs = singleshot(() => {
|
|
|
532
532
|
type: "boolean",
|
|
533
533
|
default: false,
|
|
534
534
|
},
|
|
535
|
+
help: {
|
|
536
|
+
type: "boolean",
|
|
537
|
+
default: false,
|
|
538
|
+
},
|
|
539
|
+
version: {
|
|
540
|
+
type: "boolean",
|
|
541
|
+
default: false,
|
|
542
|
+
},
|
|
535
543
|
},
|
|
536
544
|
strict: false,
|
|
537
545
|
allowPositionals: true,
|
|
@@ -2202,7 +2210,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
|
2202
2210
|
const listenGracefulShutdown$4 = singleshot(() => {
|
|
2203
2211
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
2204
2212
|
});
|
|
2205
|
-
const main$
|
|
2213
|
+
const main$9 = async () => {
|
|
2206
2214
|
if (!getEntry(import.meta.url)) {
|
|
2207
2215
|
return;
|
|
2208
2216
|
}
|
|
@@ -2213,7 +2221,7 @@ const main$7 = async () => {
|
|
|
2213
2221
|
await cli.backtestMainService.connect();
|
|
2214
2222
|
listenGracefulShutdown$4();
|
|
2215
2223
|
};
|
|
2216
|
-
main$
|
|
2224
|
+
main$9();
|
|
2217
2225
|
|
|
2218
2226
|
const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
2219
2227
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -2234,7 +2242,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
|
2234
2242
|
const listenGracefulShutdown$3 = singleshot(() => {
|
|
2235
2243
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
2236
2244
|
});
|
|
2237
|
-
const main$
|
|
2245
|
+
const main$8 = async () => {
|
|
2238
2246
|
if (!getEntry(import.meta.url)) {
|
|
2239
2247
|
return;
|
|
2240
2248
|
}
|
|
@@ -2245,7 +2253,7 @@ const main$6 = async () => {
|
|
|
2245
2253
|
cli.paperMainService.connect();
|
|
2246
2254
|
listenGracefulShutdown$3();
|
|
2247
2255
|
};
|
|
2248
|
-
main$
|
|
2256
|
+
main$8();
|
|
2249
2257
|
|
|
2250
2258
|
const BEFORE_EXIT_FN$2 = singleshot(async () => {
|
|
2251
2259
|
process.off("SIGINT", BEFORE_EXIT_FN$2);
|
|
@@ -2266,7 +2274,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
|
|
|
2266
2274
|
const listenGracefulShutdown$2 = singleshot(() => {
|
|
2267
2275
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
2268
2276
|
});
|
|
2269
|
-
const main$
|
|
2277
|
+
const main$7 = async () => {
|
|
2270
2278
|
if (!getEntry(import.meta.url)) {
|
|
2271
2279
|
return;
|
|
2272
2280
|
}
|
|
@@ -2277,7 +2285,7 @@ const main$5 = async () => {
|
|
|
2277
2285
|
await cli.liveMainService.connect();
|
|
2278
2286
|
listenGracefulShutdown$2();
|
|
2279
2287
|
};
|
|
2280
|
-
main$
|
|
2288
|
+
main$7();
|
|
2281
2289
|
|
|
2282
2290
|
const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
2283
2291
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -2287,7 +2295,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
|
2287
2295
|
const listenGracefulShutdown$1 = singleshot(() => {
|
|
2288
2296
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
2289
2297
|
});
|
|
2290
|
-
const main$
|
|
2298
|
+
const main$6 = async () => {
|
|
2291
2299
|
if (!getEntry(import.meta.url)) {
|
|
2292
2300
|
return;
|
|
2293
2301
|
}
|
|
@@ -2297,7 +2305,7 @@ const main$4 = async () => {
|
|
|
2297
2305
|
}
|
|
2298
2306
|
listenGracefulShutdown$1();
|
|
2299
2307
|
};
|
|
2300
|
-
main$
|
|
2308
|
+
main$6();
|
|
2301
2309
|
|
|
2302
2310
|
const BEFORE_EXIT_FN = singleshot(async () => {
|
|
2303
2311
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -2307,7 +2315,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
|
|
|
2307
2315
|
const listenGracefulShutdown = singleshot(() => {
|
|
2308
2316
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
2309
2317
|
});
|
|
2310
|
-
const main$
|
|
2318
|
+
const main$5 = async () => {
|
|
2311
2319
|
if (!getEntry(import.meta.url)) {
|
|
2312
2320
|
return;
|
|
2313
2321
|
}
|
|
@@ -2317,7 +2325,7 @@ const main$3 = async () => {
|
|
|
2317
2325
|
}
|
|
2318
2326
|
listenGracefulShutdown();
|
|
2319
2327
|
};
|
|
2320
|
-
main$
|
|
2328
|
+
main$5();
|
|
2321
2329
|
|
|
2322
2330
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
2323
2331
|
const keys = Object.keys(schema);
|
|
@@ -2339,7 +2347,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
2339
2347
|
}
|
|
2340
2348
|
return rows;
|
|
2341
2349
|
};
|
|
2342
|
-
const main$
|
|
2350
|
+
const main$4 = async () => {
|
|
2343
2351
|
if (!getEntry(import.meta.url)) {
|
|
2344
2352
|
return;
|
|
2345
2353
|
}
|
|
@@ -2392,6 +2400,7 @@ const main$2 = async () => {
|
|
|
2392
2400
|
await mkdir(dumpDir, { recursive: true });
|
|
2393
2401
|
await writeFile(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
2394
2402
|
console.log(`Saved: ${filePath}`);
|
|
2403
|
+
process.exit(0);
|
|
2395
2404
|
return;
|
|
2396
2405
|
}
|
|
2397
2406
|
if (values.jsonl) {
|
|
@@ -2400,6 +2409,7 @@ const main$2 = async () => {
|
|
|
2400
2409
|
await mkdir(dumpDir, { recursive: true });
|
|
2401
2410
|
await writeFile(filePath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
2402
2411
|
console.log(`Saved: ${filePath}`);
|
|
2412
|
+
process.exit(0);
|
|
2403
2413
|
return;
|
|
2404
2414
|
}
|
|
2405
2415
|
if (values.markdown) {
|
|
@@ -2407,13 +2417,15 @@ const main$2 = async () => {
|
|
|
2407
2417
|
await mkdir(dumpDir, { recursive: true });
|
|
2408
2418
|
await writeFile(filePath, await toMarkdown(signalId, plots, signalSchema), "utf-8");
|
|
2409
2419
|
console.log(`Saved: ${filePath}`);
|
|
2420
|
+
process.exit(0);
|
|
2410
2421
|
return;
|
|
2411
2422
|
}
|
|
2412
2423
|
console.log(await toMarkdown(signalId, plots, signalSchema));
|
|
2424
|
+
process.exit(0);
|
|
2413
2425
|
};
|
|
2414
|
-
main$
|
|
2426
|
+
main$4();
|
|
2415
2427
|
|
|
2416
|
-
const main$
|
|
2428
|
+
const main$3 = async () => {
|
|
2417
2429
|
if (!getEntry(import.meta.url)) {
|
|
2418
2430
|
return;
|
|
2419
2431
|
}
|
|
@@ -2447,6 +2459,7 @@ const main$1 = async () => {
|
|
|
2447
2459
|
await mkdir(dumpDir, { recursive: true });
|
|
2448
2460
|
await writeFile(filePath, JSON.stringify(candles, null, 2), "utf-8");
|
|
2449
2461
|
console.log(`Saved: ${filePath}`);
|
|
2462
|
+
process.exit(0);
|
|
2450
2463
|
return;
|
|
2451
2464
|
}
|
|
2452
2465
|
if (values.jsonl) {
|
|
@@ -2454,15 +2467,34 @@ const main$1 = async () => {
|
|
|
2454
2467
|
await mkdir(dumpDir, { recursive: true });
|
|
2455
2468
|
await writeFile(filePath, candles.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
2456
2469
|
console.log(`Saved: ${filePath}`);
|
|
2470
|
+
process.exit(0);
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
if (values.markdown) {
|
|
2474
|
+
const header = `| open | high | low | close | volume | timestamp |\n| --- | --- | --- | --- | --- | --- |`;
|
|
2475
|
+
const rows = candles.map((c) => `| ${c.open} | ${c.high} | ${c.low} | ${c.close} | ${c.volume} | ${new Date(c.timestamp).toISOString()} |`);
|
|
2476
|
+
const md = [header, ...rows].join("\n");
|
|
2477
|
+
{
|
|
2478
|
+
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
2479
|
+
await mkdir(dumpDir, { recursive: true });
|
|
2480
|
+
await writeFile(filePath, md, "utf-8");
|
|
2481
|
+
console.log(`Saved: ${filePath}`);
|
|
2482
|
+
}
|
|
2483
|
+
process.exit(0);
|
|
2457
2484
|
return;
|
|
2458
2485
|
}
|
|
2459
2486
|
console.log(JSON.stringify(candles, null, 2));
|
|
2487
|
+
process.exit(0);
|
|
2460
2488
|
};
|
|
2461
|
-
main$
|
|
2489
|
+
main$3();
|
|
2462
2490
|
|
|
2463
2491
|
const __filename = fileURLToPath(import.meta.url);
|
|
2464
2492
|
const __dirname = dirname(__filename);
|
|
2465
2493
|
const MUSTACHE_EXT = ".mustache";
|
|
2494
|
+
const MUSTACHE_RENAME = {
|
|
2495
|
+
"gitignore": ".gitignore",
|
|
2496
|
+
"package": "package.json",
|
|
2497
|
+
};
|
|
2466
2498
|
async function isDirEmpty(dirPath) {
|
|
2467
2499
|
try {
|
|
2468
2500
|
const files = await readdir(dirPath);
|
|
@@ -2484,8 +2516,12 @@ async function copyDir(srcDir, destDir, data) {
|
|
|
2484
2516
|
await copyDir(srcPath, join(destDir, entry.name), data);
|
|
2485
2517
|
continue;
|
|
2486
2518
|
}
|
|
2519
|
+
if (entry.name === ".gitkeep") {
|
|
2520
|
+
continue;
|
|
2521
|
+
}
|
|
2487
2522
|
if (entry.name.endsWith(MUSTACHE_EXT)) {
|
|
2488
|
-
const
|
|
2523
|
+
const baseName = entry.name.slice(0, -MUSTACHE_EXT.length);
|
|
2524
|
+
const destName = MUSTACHE_RENAME[baseName] ?? baseName;
|
|
2489
2525
|
const destPath = join(destDir, destName);
|
|
2490
2526
|
const template = await readFile(srcPath, "utf-8");
|
|
2491
2527
|
const rendered = Mustache.render(template, data);
|
|
@@ -2513,7 +2549,7 @@ function runScript(scriptPath, cwd) {
|
|
|
2513
2549
|
child.on("error", reject);
|
|
2514
2550
|
});
|
|
2515
2551
|
}
|
|
2516
|
-
const main = async () => {
|
|
2552
|
+
const main$2 = async () => {
|
|
2517
2553
|
if (!getEntry(import.meta.url)) {
|
|
2518
2554
|
return;
|
|
2519
2555
|
}
|
|
@@ -2523,7 +2559,7 @@ const main = async () => {
|
|
|
2523
2559
|
}
|
|
2524
2560
|
const projectName = values.output || "backtest-kit-project";
|
|
2525
2561
|
const projectPath = join(process.cwd(), projectName);
|
|
2526
|
-
const templatePath = join(__dirname, "
|
|
2562
|
+
const templatePath = join(__dirname, "../template/project");
|
|
2527
2563
|
const isEmpty = await isDirEmpty(projectPath);
|
|
2528
2564
|
if (!isEmpty) {
|
|
2529
2565
|
console.error(`Directory "${projectName}" already exists and is not empty.`);
|
|
@@ -2534,6 +2570,131 @@ const main = async () => {
|
|
|
2534
2570
|
console.log(`Fetching docs...`);
|
|
2535
2571
|
await runScript(join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
|
|
2536
2572
|
console.log(`Done! Project created at ${projectPath}`);
|
|
2573
|
+
process.exit(0);
|
|
2574
|
+
};
|
|
2575
|
+
main$2();
|
|
2576
|
+
|
|
2577
|
+
const HELP_TEXT = `
|
|
2578
|
+
@backtest-kit/cli
|
|
2579
|
+
|
|
2580
|
+
Usage:
|
|
2581
|
+
node index.mjs --<mode> [flags] [entry-point]
|
|
2582
|
+
|
|
2583
|
+
Modes:
|
|
2584
|
+
|
|
2585
|
+
--backtest <entry> Run strategy against historical candle data
|
|
2586
|
+
--paper <entry> Paper trading (live prices, no real orders)
|
|
2587
|
+
--live <entry> Live trading with real orders
|
|
2588
|
+
--pine <entry> Execute a local .pine indicator file
|
|
2589
|
+
--dump Fetch and save raw OHLCV candles
|
|
2590
|
+
--init Scaffold a new project in the current directory
|
|
2591
|
+
--help Print this help message
|
|
2592
|
+
|
|
2593
|
+
Backtest flags:
|
|
2594
|
+
|
|
2595
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2596
|
+
--strategy <string> Strategy name from addStrategySchema (default: first registered)
|
|
2597
|
+
--exchange <string> Exchange name from addExchangeSchema (default: first registered)
|
|
2598
|
+
--frame <string> Frame name from addFrameSchema (default: first registered)
|
|
2599
|
+
--cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
|
|
2600
|
+
--noCache Skip candle cache warming before the run
|
|
2601
|
+
--verbose Log every candle fetch to stdout
|
|
2602
|
+
--ui Start web dashboard at http://localhost:60050
|
|
2603
|
+
--telegram Send trade notifications to Telegram
|
|
2604
|
+
|
|
2605
|
+
Paper / Live flags:
|
|
2606
|
+
|
|
2607
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2608
|
+
--strategy <string> Strategy name (default: first registered)
|
|
2609
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2610
|
+
--verbose Log every candle fetch to stdout
|
|
2611
|
+
--ui Start web dashboard
|
|
2612
|
+
--telegram Send Telegram notifications
|
|
2613
|
+
|
|
2614
|
+
PineScript flags (--pine):
|
|
2615
|
+
|
|
2616
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2617
|
+
--timeframe <string> Candle interval (default: 15m)
|
|
2618
|
+
--limit <string> Number of candles to fetch (default: 250)
|
|
2619
|
+
--when <string> End date — ISO 8601 or Unix ms (default: now)
|
|
2620
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2621
|
+
--output <string> Output file base name without extension
|
|
2622
|
+
--json Save output as JSON array to <pine-dir>/dump/<output>.json
|
|
2623
|
+
--jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
|
|
2624
|
+
--markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
|
|
2625
|
+
|
|
2626
|
+
Only plot() calls with display=display.data_window produce output columns.
|
|
2627
|
+
Module file ./modules/pine.module is loaded automatically if it exists.
|
|
2628
|
+
|
|
2629
|
+
Candle dump flags (--dump):
|
|
2630
|
+
|
|
2631
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2632
|
+
--timeframe <string> Candle interval (default: 15m)
|
|
2633
|
+
--limit <string> Number of candles (default: 250)
|
|
2634
|
+
--when <string> End date — ISO 8601 or Unix ms (default: now)
|
|
2635
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2636
|
+
--output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
|
|
2637
|
+
--json Save as JSON array to ./dump/<output>.json
|
|
2638
|
+
--jsonl Save as JSONL to ./dump/<output>.jsonl
|
|
2639
|
+
|
|
2640
|
+
Module file ./modules/dump.module is loaded automatically if it exists.
|
|
2641
|
+
|
|
2642
|
+
Init flags (--init):
|
|
2643
|
+
|
|
2644
|
+
--output <string> Target directory name (default: backtest-kit-project)
|
|
2645
|
+
|
|
2646
|
+
Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
|
|
2647
|
+
|
|
2648
|
+
Module hooks (loaded automatically by each mode):
|
|
2649
|
+
|
|
2650
|
+
modules/backtest.module --backtest Broker adapter for backtest
|
|
2651
|
+
modules/paper.module --paper Broker adapter for paper trading
|
|
2652
|
+
modules/live.module --live Broker adapter for live trading
|
|
2653
|
+
modules/pine.module --pine Exchange schema for PineScript runs
|
|
2654
|
+
modules/dump.module --dump Exchange schema for candle dumps
|
|
2655
|
+
|
|
2656
|
+
Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
|
|
2657
|
+
|
|
2658
|
+
Environment variables:
|
|
2659
|
+
|
|
2660
|
+
CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
|
|
2661
|
+
CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
|
|
2662
|
+
CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
|
|
2663
|
+
CC_WWWROOT_PORT UI server port (default: 60050)
|
|
2664
|
+
|
|
2665
|
+
Examples:
|
|
2666
|
+
|
|
2667
|
+
node index.mjs --backtest ./content/feb_2026.strategy.ts
|
|
2668
|
+
node index.mjs --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
|
|
2669
|
+
node index.mjs --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
|
|
2670
|
+
node index.mjs --live --ui --telegram ./content/feb_2026.strategy.ts
|
|
2671
|
+
node index.mjs --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
|
|
2672
|
+
node index.mjs --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
|
|
2673
|
+
node index.mjs --init --output my-trading-bot
|
|
2674
|
+
`.trimStart();
|
|
2675
|
+
const main$1 = async () => {
|
|
2676
|
+
if (!getEntry(import.meta.url)) {
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
const { values } = getArgs();
|
|
2680
|
+
if (!values.help) {
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
process.stdout.write(HELP_TEXT);
|
|
2684
|
+
process.exit(0);
|
|
2685
|
+
};
|
|
2686
|
+
main$1();
|
|
2687
|
+
|
|
2688
|
+
const main = async () => {
|
|
2689
|
+
if (!getEntry(import.meta.url)) {
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
const { values } = getArgs();
|
|
2693
|
+
if (!values.version) {
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
process.stdout.write(`@backtest-kit/cli ${"6.0.0"}\n`);
|
|
2697
|
+
process.exit(0);
|
|
2537
2698
|
};
|
|
2538
2699
|
main();
|
|
2539
2700
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backtest-kit/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Zero-boilerplate CLI runner for backtest-kit strategies. Run backtests, paper trading, and live bots with candle cache warming, web dashboard, and Telegram notifications — no setup code required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Petr Tripolsky",
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@babel/plugin-transform-modules-umd": "7.27.1",
|
|
63
63
|
"@babel/standalone": "7.29.1",
|
|
64
|
-
"@backtest-kit/ui": "
|
|
65
|
-
"@backtest-kit/graph": "
|
|
66
|
-
"@backtest-kit/ollama": "
|
|
67
|
-
"@backtest-kit/pinets": "
|
|
68
|
-
"@backtest-kit/signals": "
|
|
64
|
+
"@backtest-kit/ui": "6.0.0",
|
|
65
|
+
"@backtest-kit/graph": "6.0.0",
|
|
66
|
+
"@backtest-kit/ollama": "6.0.0",
|
|
67
|
+
"@backtest-kit/pinets": "6.0.0",
|
|
68
|
+
"@backtest-kit/signals": "6.0.0",
|
|
69
69
|
"@rollup/plugin-replace": "6.0.3",
|
|
70
70
|
"@rollup/plugin-typescript": "11.1.6",
|
|
71
71
|
"@types/image-size": "0.7.0",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@types/mustache": "4.2.6",
|
|
74
74
|
"@types/node": "22.9.0",
|
|
75
75
|
"@types/stack-trace": "0.0.33",
|
|
76
|
-
"backtest-kit": "
|
|
76
|
+
"backtest-kit": "6.0.0",
|
|
77
77
|
"glob": "11.0.1",
|
|
78
78
|
"markdown-it": "14.1.1",
|
|
79
79
|
"rimraf": "6.0.1",
|
|
@@ -88,12 +88,12 @@
|
|
|
88
88
|
"peerDependencies": {
|
|
89
89
|
"@babel/plugin-transform-modules-umd": "^7.27.1",
|
|
90
90
|
"@babel/standalone": "^7.29.1",
|
|
91
|
-
"@backtest-kit/ui": "^
|
|
92
|
-
"@backtest-kit/graph": "^
|
|
93
|
-
"@backtest-kit/ollama": "^
|
|
94
|
-
"@backtest-kit/pinets": "^
|
|
95
|
-
"@backtest-kit/signals": "^
|
|
96
|
-
"backtest-kit": "^
|
|
91
|
+
"@backtest-kit/ui": "^6.0.0",
|
|
92
|
+
"@backtest-kit/graph": "^6.0.0",
|
|
93
|
+
"@backtest-kit/ollama": "^6.0.0",
|
|
94
|
+
"@backtest-kit/pinets": "^6.0.0",
|
|
95
|
+
"@backtest-kit/signals": "^6.0.0",
|
|
96
|
+
"backtest-kit": "^6.0.0",
|
|
97
97
|
"markdown-it": "^14.1.1",
|
|
98
98
|
"typescript": "^5.0.0"
|
|
99
99
|
},
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# 🧿 Backtest Kit Project
|
|
2
|
+
|
|
3
|
+
> A TypeScript framework for backtesting and live trading strategies on multi-asset, crypto, forex or [DEX (peer-to-peer marketplace)](https://en.wikipedia.org/wiki/Decentralized_finance#Decentralized_exchanges), spot, futures with crash-safe persistence, signal validation, and AI optimization.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
[](https://deepwiki.com/tripolskypetr/backtest-kit)
|
|
8
|
+
[](https://npmjs.org/package/backtest-kit)
|
|
9
|
+
[]()
|
|
10
|
+
[](https://github.com/tripolskypetr/backtest-kit/actions/workflows/webpack.yml)
|
|
11
|
+
|
|
12
|
+
A minimal project scaffold for [backtest-kit](https://github.com/tripolskypetr/backtest-kit). All infrastructure (exchange registration, candle caching, runner, UI, Telegram) is handled by `@backtest-kit/cli` — this project contains only your strategy files.
|
|
13
|
+
|
|
14
|
+
## 📋 Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm start # Run the CLI (append flags below)
|
|
18
|
+
npm run sync:lib # Refresh library docs in docs/lib/
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 🏃 Running Modes
|
|
22
|
+
|
|
23
|
+
All modes are invoked via `npm start -- <flags> <entry-point>`.
|
|
24
|
+
|
|
25
|
+
### 🧪 Backtest
|
|
26
|
+
|
|
27
|
+
Runs the strategy against historical candle data defined by a `FrameSchema`.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm start -- --backtest --symbol BTCUSDT --strategy feb_2026_strategy --exchange ccxt-exchange --frame feb_2026_frame ./content/feb_2026.strategy.ts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Flag | Type | Default | Description |
|
|
34
|
+
|------|------|---------|-------------|
|
|
35
|
+
| `--backtest` | boolean | — | Enable backtest mode |
|
|
36
|
+
| `--symbol` | string | `BTCUSDT` | Trading pair |
|
|
37
|
+
| `--strategy` | string | first registered | Strategy name from `addStrategySchema` |
|
|
38
|
+
| `--exchange` | string | first registered | Exchange name from `addExchangeSchema` |
|
|
39
|
+
| `--frame` | string | first registered | Frame name from `addFrameSchema` |
|
|
40
|
+
| `--cacheInterval` | string | `1m, 15m, 30m, 1h, 4h` | Comma-separated intervals to pre-cache before the run |
|
|
41
|
+
| `--noCache` | boolean | `false` | Skip candle cache warming |
|
|
42
|
+
| `--verbose` | boolean | `false` | Log every candle fetch to stdout |
|
|
43
|
+
| `--ui` | boolean | `false` | Start web dashboard at `http://localhost:60050` |
|
|
44
|
+
| `--telegram` | boolean | `false` | Send trade notifications to Telegram |
|
|
45
|
+
|
|
46
|
+
Before the backtest starts, the CLI warms the candle cache for every interval in `--cacheInterval`. On subsequent runs the cache is used directly — no extra API calls. Pass `--noCache` to skip this step.
|
|
47
|
+
|
|
48
|
+
Module file `./modules/backtest.module.ts` (or `.mjs`) is loaded automatically if it exists.
|
|
49
|
+
|
|
50
|
+
### 📄 Paper Trading
|
|
51
|
+
|
|
52
|
+
Connects to the live exchange but places no real orders. Identical code path to `--live` — safe for strategy validation.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm start -- --paper --symbol BTCUSDT --strategy feb_2026_strategy --exchange ccxt-exchange ./content/feb_2026.strategy.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Flag | Type | Default | Description |
|
|
59
|
+
|------|------|---------|-------------|
|
|
60
|
+
| `--paper` | boolean | — | Enable paper trading mode |
|
|
61
|
+
| `--symbol` | string | `BTCUSDT` | Trading pair |
|
|
62
|
+
| `--strategy` | string | first registered | Strategy name |
|
|
63
|
+
| `--exchange` | string | first registered | Exchange name |
|
|
64
|
+
| `--verbose` | boolean | `false` | Log every candle fetch to stdout |
|
|
65
|
+
| `--ui` | boolean | `false` | Start web dashboard |
|
|
66
|
+
| `--telegram` | boolean | `false` | Enable Telegram notifications |
|
|
67
|
+
|
|
68
|
+
Module file `./modules/paper.module.ts` is loaded automatically if it exists.
|
|
69
|
+
|
|
70
|
+
### 📈 Live Trading
|
|
71
|
+
|
|
72
|
+
Deploys a real trading bot. Requires exchange API keys in `.env`.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm start -- --live --symbol BTCUSDT --ui --telegram ./content/feb_2026.strategy.ts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Flag | Type | Default | Description |
|
|
79
|
+
|------|------|---------|-------------|
|
|
80
|
+
| `--live` | boolean | — | Enable live trading mode |
|
|
81
|
+
| `--symbol` | string | `BTCUSDT` | Trading pair |
|
|
82
|
+
| `--strategy` | string | first registered | Strategy name |
|
|
83
|
+
| `--exchange` | string | first registered | Exchange name |
|
|
84
|
+
| `--verbose` | boolean | `false` | Log every candle fetch to stdout |
|
|
85
|
+
| `--ui` | boolean | `false` | Start web dashboard |
|
|
86
|
+
| `--telegram` | boolean | `false` | Enable Telegram notifications |
|
|
87
|
+
|
|
88
|
+
Module file `./modules/live.module.ts` is loaded automatically if it exists. Use it to register a `Broker` adapter that intercepts every trade mutation before internal state changes — exchange rejection rolls back the operation atomically.
|
|
89
|
+
|
|
90
|
+
## 🌲 Running PineScript Indicators (`--pine`)
|
|
91
|
+
|
|
92
|
+
Executes a local `.pine` file against a real exchange and prints the output as a Markdown table or saves it to a file.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm start -- --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --when "2026-02-28T00:00:00.000Z" --jsonl
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Output file is created at `./math/dump/<name>.jsonl` (next to the `.pine` file).
|
|
99
|
+
|
|
100
|
+
| Flag | Type | Default | Description |
|
|
101
|
+
|------|------|---------|-------------|
|
|
102
|
+
| `--pine` | boolean | — | Enable PineScript execution mode |
|
|
103
|
+
| `--symbol` | string | `BTCUSDT` | Trading pair |
|
|
104
|
+
| `--timeframe` | string | `15m` | Candle interval |
|
|
105
|
+
| `--limit` | string | `250` | Number of candles to fetch |
|
|
106
|
+
| `--when` | string | now | End date — ISO 8601 or Unix ms |
|
|
107
|
+
| `--exchange` | string | first registered | Exchange name |
|
|
108
|
+
| `--output` | string | `.pine` file name | Output file base name (no extension) |
|
|
109
|
+
| `--json` | boolean | `false` | Save output as JSON array |
|
|
110
|
+
| `--jsonl` | boolean | `false` | Save output as JSONL (one row per line) |
|
|
111
|
+
| `--markdown` | boolean | `false` | Save output as Markdown table |
|
|
112
|
+
|
|
113
|
+
Module file `./modules/pine.module.ts` is loaded automatically. The project includes it pre-configured with CCXT Binance. Override it to use a different exchange.
|
|
114
|
+
|
|
115
|
+
Only `plot()` calls with `display=display.data_window` produce output columns:
|
|
116
|
+
|
|
117
|
+
```pine
|
|
118
|
+
plot(close, "Close", display=display.data_window)
|
|
119
|
+
plot(position, "Position", display=display.data_window)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 💾 Dumping Raw Candles (`--dump`)
|
|
123
|
+
|
|
124
|
+
Fetches raw OHLCV candles from an exchange and saves them to a file.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm start -- --dump --timeframe 15m --limit 500 --when "2026-02-28T00:00:00.000Z" --jsonl
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Output file is created at `./dump/<name>.jsonl`.
|
|
131
|
+
|
|
132
|
+
| Flag | Type | Default | Description |
|
|
133
|
+
|------|------|---------|-------------|
|
|
134
|
+
| `--dump` | boolean | — | Enable candle dump mode |
|
|
135
|
+
| `--symbol` | string | `BTCUSDT` | Trading pair |
|
|
136
|
+
| `--timeframe` | string | `15m` | Candle interval |
|
|
137
|
+
| `--limit` | string | `250` | Number of candles |
|
|
138
|
+
| `--when` | string | now | End date — ISO 8601 or Unix ms |
|
|
139
|
+
| `--exchange` | string | first registered | Exchange name |
|
|
140
|
+
| `--output` | string | `{SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP}` | Output file base name |
|
|
141
|
+
| `--json` | boolean | `false` | Save as JSON array |
|
|
142
|
+
| `--jsonl` | boolean | `false` | Save as JSONL |
|
|
143
|
+
|
|
144
|
+
Module file `./modules/dump.module.ts` is loaded automatically. The project includes it pre-configured with CCXT Binance.
|
|
145
|
+
|
|
146
|
+
## 🧩 Module Hooks
|
|
147
|
+
|
|
148
|
+
| File | Loaded by mode | Purpose |
|
|
149
|
+
|------|----------------|---------|
|
|
150
|
+
| `modules/backtest.module.ts` | `--backtest` | Register a `Broker` adapter for backtest |
|
|
151
|
+
| `modules/paper.module.ts` | `--paper` | Register a `Broker` adapter for paper trading |
|
|
152
|
+
| `modules/live.module.ts` | `--live` | Register a `Broker` adapter for live trading |
|
|
153
|
+
| `modules/pine.module.ts` | `--pine` | Register an exchange schema for PineScript runs |
|
|
154
|
+
| `modules/dump.module.ts` | `--dump` | Register an exchange schema for candle dumps |
|
|
155
|
+
|
|
156
|
+
All files are optional — a missing module is a soft warning, not an error. Extensions `.ts`, `.mjs`, `.cjs` are tried automatically.
|
|
157
|
+
|
|
158
|
+
## 🌍 Environment Variables
|
|
159
|
+
|
|
160
|
+
Create a `.env` file in the project root:
|
|
161
|
+
|
|
162
|
+
```env
|
|
163
|
+
# Telegram notifications (required for --telegram)
|
|
164
|
+
CC_TELEGRAM_TOKEN=your_bot_token_here
|
|
165
|
+
CC_TELEGRAM_CHANNEL=-100123456789
|
|
166
|
+
|
|
167
|
+
# Web UI server (optional, defaults shown)
|
|
168
|
+
CC_WWWROOT_HOST=0.0.0.0
|
|
169
|
+
CC_WWWROOT_PORT=60050
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
| Variable | Default | Description |
|
|
173
|
+
|----------|---------|-------------|
|
|
174
|
+
| `CC_TELEGRAM_TOKEN` | — | Telegram bot token (from @BotFather) |
|
|
175
|
+
| `CC_TELEGRAM_CHANNEL` | — | Telegram channel or chat ID |
|
|
176
|
+
| `CC_WWWROOT_HOST` | `0.0.0.0` | UI server bind address |
|
|
177
|
+
| `CC_WWWROOT_PORT` | `60050` | UI server port |
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
## 🗂️ Project Structure
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
├── content/ # Strategy entry points (.ts)
|
|
184
|
+
│ └── feb_2026.strategy.ts
|
|
185
|
+
├── docs/ # Documentation
|
|
186
|
+
│ ├── lib/ # Auto-fetched library READMEs (via sync:lib)
|
|
187
|
+
│ └── *.md # Backtest Kit how-to guides
|
|
188
|
+
├── math/ # PineScript indicator files (.pine)
|
|
189
|
+
│ └── feb_2026.pine
|
|
190
|
+
├── modules/ # Side-effect module hooks (loaded automatically)
|
|
191
|
+
│ ├── dump.module.ts # Exchange schema for --dump mode
|
|
192
|
+
│ └── pine.module.ts # Exchange schema for --pine mode
|
|
193
|
+
├── report/ # Strategy research reports (.md)
|
|
194
|
+
│ └── feb_2026.md
|
|
195
|
+
├── scripts/
|
|
196
|
+
│ └── fetch_docs.mjs # Downloads library READMEs into docs/lib/
|
|
197
|
+
├── CLAUDE.md # AI-agent guide for writing strategies
|
|
198
|
+
└── package.json
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## 📚 Updating Library Documentation
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npm run sync:lib
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Downloads the latest README files for all bundled libraries into `docs/lib/`. Run this after updating package versions or when you want fresh documentation available to the AI agent.
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"license": "ISC",
|
|
13
13
|
"type": "commonjs",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@backtest-kit/cli": "^
|
|
16
|
-
"@backtest-kit/graph": "^
|
|
17
|
-
"@backtest-kit/pinets": "^
|
|
18
|
-
"@backtest-kit/ui": "^
|
|
15
|
+
"@backtest-kit/cli": "^6.0.0",
|
|
16
|
+
"@backtest-kit/graph": "^6.0.0",
|
|
17
|
+
"@backtest-kit/pinets": "^6.0.0",
|
|
18
|
+
"@backtest-kit/ui": "^6.0.0",
|
|
19
19
|
"agent-swarm-kit": "^1.3.0",
|
|
20
|
-
"backtest-kit": "^
|
|
20
|
+
"backtest-kit": "^6.0.0",
|
|
21
21
|
"functools-kit": "^1.0.95",
|
|
22
22
|
"garch": "^1.2.3",
|
|
23
23
|
"get-moment-stamp": "^1.1.2",
|
|
@@ -44,15 +44,24 @@ const OUT_DIR = path.resolve("./docs/lib");
|
|
|
44
44
|
|
|
45
45
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
const main = async () => {
|
|
48
|
+
for (const lib of LIBRARY_LIST) {
|
|
49
|
+
const res = await fetch(lib.readme);
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
console.error(`Failed to fetch ${lib.name}: ${res.status} ${res.statusText}`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const text = await res.text();
|
|
55
|
+
const fileName = lib.name.replace(/\//g, "__") + ".md";
|
|
56
|
+
const outPath = path.join(OUT_DIR, fileName);
|
|
57
|
+
fs.writeFileSync(outPath, text, "utf-8");
|
|
58
|
+
console.log(`Saved ${lib.name} -> ${outPath}`);
|
|
52
59
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await main();
|
|
64
|
+
} catch {
|
|
65
|
+
console.log("Failed to fetch docs")
|
|
58
66
|
}
|
|
67
|
+
|