@backtest-kit/cli 5.11.1 → 6.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/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$7 = async () => {
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$7();
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$6 = async () => {
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$6();
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$5 = async () => {
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$5();
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$4 = async () => {
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$4();
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$3 = async () => {
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$3();
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$2 = async () => {
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$2();
2455
+ main$4();
2444
2456
 
2445
- const main$1 = async () => {
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,21 @@ 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);
2486
2500
  return;
2487
2501
  }
2488
2502
  console.log(JSON.stringify(candles, null, 2));
2503
+ process.exit(0);
2489
2504
  };
2490
- main$1();
2505
+ main$3();
2491
2506
 
2492
2507
  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
2508
  const __dirname$1 = path.dirname(__filename$1);
2494
2509
  const MUSTACHE_EXT = ".mustache";
2510
+ const MUSTACHE_RENAME = {
2511
+ "gitignore": ".gitignore",
2512
+ "package": "package.json",
2513
+ };
2495
2514
  async function isDirEmpty(dirPath) {
2496
2515
  try {
2497
2516
  const files = await fs$1.readdir(dirPath);
@@ -2513,8 +2532,12 @@ async function copyDir(srcDir, destDir, data) {
2513
2532
  await copyDir(srcPath, path.join(destDir, entry.name), data);
2514
2533
  continue;
2515
2534
  }
2535
+ if (entry.name === ".gitkeep") {
2536
+ continue;
2537
+ }
2516
2538
  if (entry.name.endsWith(MUSTACHE_EXT)) {
2517
- const destName = entry.name.slice(0, -MUSTACHE_EXT.length);
2539
+ const baseName = entry.name.slice(0, -MUSTACHE_EXT.length);
2540
+ const destName = MUSTACHE_RENAME[baseName] ?? baseName;
2518
2541
  const destPath = path.join(destDir, destName);
2519
2542
  const template = await fs$1.readFile(srcPath, "utf-8");
2520
2543
  const rendered = Mustache.render(template, data);
@@ -2542,7 +2565,7 @@ function runScript(scriptPath, cwd) {
2542
2565
  child.on("error", reject);
2543
2566
  });
2544
2567
  }
2545
- const main = async () => {
2568
+ const main$2 = async () => {
2546
2569
  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
2570
  return;
2548
2571
  }
@@ -2552,7 +2575,7 @@ const main = async () => {
2552
2575
  }
2553
2576
  const projectName = values.output || "backtest-kit-project";
2554
2577
  const projectPath = path.join(process.cwd(), projectName);
2555
- const templatePath = path.join(__dirname$1, "../../template/project");
2578
+ const templatePath = path.join(__dirname$1, "../template/project");
2556
2579
  const isEmpty = await isDirEmpty(projectPath);
2557
2580
  if (!isEmpty) {
2558
2581
  console.error(`Directory "${projectName}" already exists and is not empty.`);
@@ -2563,6 +2586,131 @@ const main = async () => {
2563
2586
  console.log(`Fetching docs...`);
2564
2587
  await runScript(path.join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
2565
2588
  console.log(`Done! Project created at ${projectPath}`);
2589
+ process.exit(0);
2590
+ };
2591
+ main$2();
2592
+
2593
+ const HELP_TEXT = `
2594
+ @backtest-kit/cli
2595
+
2596
+ Usage:
2597
+ node index.mjs --<mode> [flags] [entry-point]
2598
+
2599
+ Modes:
2600
+
2601
+ --backtest <entry> Run strategy against historical candle data
2602
+ --paper <entry> Paper trading (live prices, no real orders)
2603
+ --live <entry> Live trading with real orders
2604
+ --pine <entry> Execute a local .pine indicator file
2605
+ --dump Fetch and save raw OHLCV candles
2606
+ --init Scaffold a new project in the current directory
2607
+ --help Print this help message
2608
+
2609
+ Backtest flags:
2610
+
2611
+ --symbol <string> Trading pair (default: BTCUSDT)
2612
+ --strategy <string> Strategy name from addStrategySchema (default: first registered)
2613
+ --exchange <string> Exchange name from addExchangeSchema (default: first registered)
2614
+ --frame <string> Frame name from addFrameSchema (default: first registered)
2615
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2616
+ --noCache Skip candle cache warming before the run
2617
+ --verbose Log every candle fetch to stdout
2618
+ --ui Start web dashboard at http://localhost:60050
2619
+ --telegram Send trade notifications to Telegram
2620
+
2621
+ Paper / Live flags:
2622
+
2623
+ --symbol <string> Trading pair (default: BTCUSDT)
2624
+ --strategy <string> Strategy name (default: first registered)
2625
+ --exchange <string> Exchange name (default: first registered)
2626
+ --verbose Log every candle fetch to stdout
2627
+ --ui Start web dashboard
2628
+ --telegram Send Telegram notifications
2629
+
2630
+ PineScript flags (--pine):
2631
+
2632
+ --symbol <string> Trading pair (default: BTCUSDT)
2633
+ --timeframe <string> Candle interval (default: 15m)
2634
+ --limit <string> Number of candles to fetch (default: 250)
2635
+ --when <string> End date — ISO 8601 or Unix ms (default: now)
2636
+ --exchange <string> Exchange name (default: first registered)
2637
+ --output <string> Output file base name without extension
2638
+ --json Save output as JSON array to <pine-dir>/dump/<output>.json
2639
+ --jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
2640
+ --markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
2641
+
2642
+ Only plot() calls with display=display.data_window produce output columns.
2643
+ Module file ./modules/pine.module is loaded automatically if it exists.
2644
+
2645
+ Candle dump flags (--dump):
2646
+
2647
+ --symbol <string> Trading pair (default: BTCUSDT)
2648
+ --timeframe <string> Candle interval (default: 15m)
2649
+ --limit <string> Number of candles (default: 250)
2650
+ --when <string> End date — ISO 8601 or Unix ms (default: now)
2651
+ --exchange <string> Exchange name (default: first registered)
2652
+ --output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
2653
+ --json Save as JSON array to ./dump/<output>.json
2654
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
2655
+
2656
+ Module file ./modules/dump.module is loaded automatically if it exists.
2657
+
2658
+ Init flags (--init):
2659
+
2660
+ --output <string> Target directory name (default: backtest-kit-project)
2661
+
2662
+ Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
2663
+
2664
+ Module hooks (loaded automatically by each mode):
2665
+
2666
+ modules/backtest.module --backtest Broker adapter for backtest
2667
+ modules/paper.module --paper Broker adapter for paper trading
2668
+ modules/live.module --live Broker adapter for live trading
2669
+ modules/pine.module --pine Exchange schema for PineScript runs
2670
+ modules/dump.module --dump Exchange schema for candle dumps
2671
+
2672
+ Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
2673
+
2674
+ Environment variables:
2675
+
2676
+ CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
2677
+ CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
2678
+ CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
2679
+ CC_WWWROOT_PORT UI server port (default: 60050)
2680
+
2681
+ Examples:
2682
+
2683
+ node index.mjs --backtest ./content/feb_2026.strategy.ts
2684
+ node index.mjs --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
2685
+ node index.mjs --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
2686
+ node index.mjs --live --ui --telegram ./content/feb_2026.strategy.ts
2687
+ node index.mjs --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
2688
+ node index.mjs --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
2689
+ node index.mjs --init --output my-trading-bot
2690
+ `.trimStart();
2691
+ const main$1 = async () => {
2692
+ 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)))) {
2693
+ return;
2694
+ }
2695
+ const { values } = getArgs();
2696
+ if (!values.help) {
2697
+ return;
2698
+ }
2699
+ process.stdout.write(HELP_TEXT);
2700
+ process.exit(0);
2701
+ };
2702
+ main$1();
2703
+
2704
+ const main = 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.version) {
2710
+ return;
2711
+ }
2712
+ process.stdout.write(`@backtest-kit/cli ${"6.0.0"}\n`);
2713
+ process.exit(0);
2566
2714
  };
2567
2715
  main();
2568
2716
 
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$7 = async () => {
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$7();
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$6 = async () => {
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$6();
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$5 = async () => {
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$5();
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$4 = async () => {
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$4();
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$3 = async () => {
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$3();
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$2 = async () => {
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$2();
2426
+ main$4();
2415
2427
 
2416
- const main$1 = async () => {
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,21 @@ 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);
2457
2471
  return;
2458
2472
  }
2459
2473
  console.log(JSON.stringify(candles, null, 2));
2474
+ process.exit(0);
2460
2475
  };
2461
- main$1();
2476
+ main$3();
2462
2477
 
2463
2478
  const __filename = fileURLToPath(import.meta.url);
2464
2479
  const __dirname = dirname(__filename);
2465
2480
  const MUSTACHE_EXT = ".mustache";
2481
+ const MUSTACHE_RENAME = {
2482
+ "gitignore": ".gitignore",
2483
+ "package": "package.json",
2484
+ };
2466
2485
  async function isDirEmpty(dirPath) {
2467
2486
  try {
2468
2487
  const files = await readdir(dirPath);
@@ -2484,8 +2503,12 @@ async function copyDir(srcDir, destDir, data) {
2484
2503
  await copyDir(srcPath, join(destDir, entry.name), data);
2485
2504
  continue;
2486
2505
  }
2506
+ if (entry.name === ".gitkeep") {
2507
+ continue;
2508
+ }
2487
2509
  if (entry.name.endsWith(MUSTACHE_EXT)) {
2488
- const destName = entry.name.slice(0, -MUSTACHE_EXT.length);
2510
+ const baseName = entry.name.slice(0, -MUSTACHE_EXT.length);
2511
+ const destName = MUSTACHE_RENAME[baseName] ?? baseName;
2489
2512
  const destPath = join(destDir, destName);
2490
2513
  const template = await readFile(srcPath, "utf-8");
2491
2514
  const rendered = Mustache.render(template, data);
@@ -2513,7 +2536,7 @@ function runScript(scriptPath, cwd) {
2513
2536
  child.on("error", reject);
2514
2537
  });
2515
2538
  }
2516
- const main = async () => {
2539
+ const main$2 = async () => {
2517
2540
  if (!getEntry(import.meta.url)) {
2518
2541
  return;
2519
2542
  }
@@ -2523,7 +2546,7 @@ const main = async () => {
2523
2546
  }
2524
2547
  const projectName = values.output || "backtest-kit-project";
2525
2548
  const projectPath = join(process.cwd(), projectName);
2526
- const templatePath = join(__dirname, "../../template/project");
2549
+ const templatePath = join(__dirname, "../template/project");
2527
2550
  const isEmpty = await isDirEmpty(projectPath);
2528
2551
  if (!isEmpty) {
2529
2552
  console.error(`Directory "${projectName}" already exists and is not empty.`);
@@ -2534,6 +2557,131 @@ const main = async () => {
2534
2557
  console.log(`Fetching docs...`);
2535
2558
  await runScript(join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
2536
2559
  console.log(`Done! Project created at ${projectPath}`);
2560
+ process.exit(0);
2561
+ };
2562
+ main$2();
2563
+
2564
+ const HELP_TEXT = `
2565
+ @backtest-kit/cli
2566
+
2567
+ Usage:
2568
+ node index.mjs --<mode> [flags] [entry-point]
2569
+
2570
+ Modes:
2571
+
2572
+ --backtest <entry> Run strategy against historical candle data
2573
+ --paper <entry> Paper trading (live prices, no real orders)
2574
+ --live <entry> Live trading with real orders
2575
+ --pine <entry> Execute a local .pine indicator file
2576
+ --dump Fetch and save raw OHLCV candles
2577
+ --init Scaffold a new project in the current directory
2578
+ --help Print this help message
2579
+
2580
+ Backtest flags:
2581
+
2582
+ --symbol <string> Trading pair (default: BTCUSDT)
2583
+ --strategy <string> Strategy name from addStrategySchema (default: first registered)
2584
+ --exchange <string> Exchange name from addExchangeSchema (default: first registered)
2585
+ --frame <string> Frame name from addFrameSchema (default: first registered)
2586
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2587
+ --noCache Skip candle cache warming before the run
2588
+ --verbose Log every candle fetch to stdout
2589
+ --ui Start web dashboard at http://localhost:60050
2590
+ --telegram Send trade notifications to Telegram
2591
+
2592
+ Paper / Live flags:
2593
+
2594
+ --symbol <string> Trading pair (default: BTCUSDT)
2595
+ --strategy <string> Strategy name (default: first registered)
2596
+ --exchange <string> Exchange name (default: first registered)
2597
+ --verbose Log every candle fetch to stdout
2598
+ --ui Start web dashboard
2599
+ --telegram Send Telegram notifications
2600
+
2601
+ PineScript flags (--pine):
2602
+
2603
+ --symbol <string> Trading pair (default: BTCUSDT)
2604
+ --timeframe <string> Candle interval (default: 15m)
2605
+ --limit <string> Number of candles to fetch (default: 250)
2606
+ --when <string> End date — ISO 8601 or Unix ms (default: now)
2607
+ --exchange <string> Exchange name (default: first registered)
2608
+ --output <string> Output file base name without extension
2609
+ --json Save output as JSON array to <pine-dir>/dump/<output>.json
2610
+ --jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
2611
+ --markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
2612
+
2613
+ Only plot() calls with display=display.data_window produce output columns.
2614
+ Module file ./modules/pine.module is loaded automatically if it exists.
2615
+
2616
+ Candle dump flags (--dump):
2617
+
2618
+ --symbol <string> Trading pair (default: BTCUSDT)
2619
+ --timeframe <string> Candle interval (default: 15m)
2620
+ --limit <string> Number of candles (default: 250)
2621
+ --when <string> End date — ISO 8601 or Unix ms (default: now)
2622
+ --exchange <string> Exchange name (default: first registered)
2623
+ --output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
2624
+ --json Save as JSON array to ./dump/<output>.json
2625
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
2626
+
2627
+ Module file ./modules/dump.module is loaded automatically if it exists.
2628
+
2629
+ Init flags (--init):
2630
+
2631
+ --output <string> Target directory name (default: backtest-kit-project)
2632
+
2633
+ Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
2634
+
2635
+ Module hooks (loaded automatically by each mode):
2636
+
2637
+ modules/backtest.module --backtest Broker adapter for backtest
2638
+ modules/paper.module --paper Broker adapter for paper trading
2639
+ modules/live.module --live Broker adapter for live trading
2640
+ modules/pine.module --pine Exchange schema for PineScript runs
2641
+ modules/dump.module --dump Exchange schema for candle dumps
2642
+
2643
+ Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
2644
+
2645
+ Environment variables:
2646
+
2647
+ CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
2648
+ CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
2649
+ CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
2650
+ CC_WWWROOT_PORT UI server port (default: 60050)
2651
+
2652
+ Examples:
2653
+
2654
+ node index.mjs --backtest ./content/feb_2026.strategy.ts
2655
+ node index.mjs --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
2656
+ node index.mjs --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
2657
+ node index.mjs --live --ui --telegram ./content/feb_2026.strategy.ts
2658
+ node index.mjs --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
2659
+ node index.mjs --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
2660
+ node index.mjs --init --output my-trading-bot
2661
+ `.trimStart();
2662
+ const main$1 = async () => {
2663
+ if (!getEntry(import.meta.url)) {
2664
+ return;
2665
+ }
2666
+ const { values } = getArgs();
2667
+ if (!values.help) {
2668
+ return;
2669
+ }
2670
+ process.stdout.write(HELP_TEXT);
2671
+ process.exit(0);
2672
+ };
2673
+ main$1();
2674
+
2675
+ const main = async () => {
2676
+ if (!getEntry(import.meta.url)) {
2677
+ return;
2678
+ }
2679
+ const { values } = getArgs();
2680
+ if (!values.version) {
2681
+ return;
2682
+ }
2683
+ process.stdout.write(`@backtest-kit/cli ${"6.0.0"}\n`);
2684
+ process.exit(0);
2537
2685
  };
2538
2686
  main();
2539
2687
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/cli",
3
- "version": "5.11.1",
3
+ "version": "6.0.0",
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": "5.11.0",
65
- "@backtest-kit/graph": "5.11.0",
66
- "@backtest-kit/ollama": "5.11.0",
67
- "@backtest-kit/pinets": "5.11.0",
68
- "@backtest-kit/signals": "5.11.0",
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": "5.11.0",
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": "^5.11.0",
92
- "@backtest-kit/graph": "^5.11.0",
93
- "@backtest-kit/ollama": "^5.11.0",
94
- "@backtest-kit/pinets": "^5.11.0",
95
- "@backtest-kit/signals": "^5.11.0",
96
- "backtest-kit": "^5.11.0",
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
+ ![screenshot](https://raw.githubusercontent.com/tripolskypetr/backtest-kit/HEAD/assets/screenshots/screenshot16.png)
6
+
7
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/tripolskypetr/backtest-kit)
8
+ [![npm](https://img.shields.io/npm/v/backtest-kit.svg?style=flat-square)](https://npmjs.org/package/backtest-kit)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)]()
10
+ [![Build](https://github.com/tripolskypetr/backtest-kit/actions/workflows/webpack.yml/badge.svg)](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.