@backtest-kit/cli 5.11.0 → 5.11.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/README.md CHANGED
@@ -38,6 +38,9 @@ Point the CLI at your strategy file, choose a mode, and it handles exchange conn
38
38
  | **Live** | `--live` | Real trades via exchange API |
39
39
  | **UI Dashboard** | `--ui` | Web dashboard at `http://localhost:60050` |
40
40
  | **Telegram** | `--telegram` | Trade notifications with price charts |
41
+ | **PineScript** | `--pine` | Run a local `.pine` indicator against exchange data |
42
+ | **Candle Dump** | `--dump` | Fetch and save raw OHLCV candles to a file |
43
+ | **Init Project** | `--init` | Scaffold a new backtest-kit project |
41
44
 
42
45
  ## 🚀 Installation
43
46
 
@@ -705,6 +708,93 @@ Or add it to `package.json`:
705
708
  npx @backtest-kit/cli --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
706
709
  ```
707
710
 
711
+ ## 🗂️ Scaffolding a New Project (`--init`)
712
+
713
+ `@backtest-kit/cli` can bootstrap a ready-to-use project directory with a pre-configured layout, example strategy files, and all documentation fetched automatically.
714
+
715
+ ### CLI Flags
716
+
717
+ | Flag | Type | Description |
718
+ |------|------|-------------|
719
+ | `--init` | boolean | Scaffold a new project |
720
+ | `--output` | string | Target directory name (default: `backtest-kit-project`) |
721
+
722
+ ### Usage
723
+
724
+ ```bash
725
+ npx @backtest-kit/cli --init
726
+ ```
727
+
728
+ Creates `./backtest-kit-project/` in the current working directory.
729
+
730
+ Override the directory name with `--output`:
731
+
732
+ ```bash
733
+ npx @backtest-kit/cli --init --output my-trading-bot
734
+ ```
735
+
736
+ Creates `./my-trading-bot/`.
737
+
738
+ The target directory must not exist or must be empty — the command aborts if it contains any files.
739
+
740
+ ### Generated Project Structure
741
+
742
+ ```
743
+ backtest-kit-project/
744
+ ├── package.json # pre-configured with all backtest-kit dependencies
745
+ ├── .gitignore
746
+ ├── CLAUDE.md # AI-agent guide for writing strategies
747
+ ├── content/
748
+ │ └── feb_2026.strategy.ts # example strategy entry point
749
+ ├── docs/
750
+ │ ├── lib/ # fetched automatically (see below)
751
+ │ ├── backtest_actions.md
752
+ │ ├── backtest_graph_pattern.md
753
+ │ ├── backtest_logging_jsonl.md
754
+ │ ├── backtest_pinets_usage.md
755
+ │ ├── backtest_risk_async.md
756
+ │ ├── backtest_strategy_structure.md
757
+ │ ├── pine_debug.md
758
+ │ └── pine_indicator_warmup.md
759
+ ├── math/
760
+ │ └── feb_2026.pine # example PineScript indicator
761
+ ├── modules/
762
+ │ ├── dump.module.ts # exchange schema for --dump mode
763
+ │ └── pine.module.ts # exchange schema for --pine mode
764
+ ├── report/
765
+ │ └── feb_2026.md # example strategy research report
766
+ └── scripts/
767
+ └── fetch_docs.mjs # utility: downloads library READMEs into docs/lib/
768
+ ```
769
+
770
+ ### Automatic Documentation Fetch
771
+
772
+ After scaffolding, the CLI immediately runs `scripts/fetch_docs.mjs` inside the new project, which downloads the latest README files for all bundled libraries into `docs/lib/`:
773
+
774
+ | File | Source |
775
+ |------|--------|
776
+ | `backtest-kit.md` | `backtest-kit` README |
777
+ | `backtest-kit__graph.md` | `@backtest-kit/graph` README |
778
+ | `backtest-kit__pinets.md` | `@backtest-kit/pinets` README |
779
+ | `backtest-kit__cli.md` | `@backtest-kit/cli` README |
780
+ | `garch.md` | `garch` README |
781
+ | `volume-anomaly.md` | `volume-anomaly` README |
782
+ | `agent-swarm-kit.md` | `agent-swarm-kit` README |
783
+ | `functools-kit.md` | `functools-kit` README |
784
+
785
+ You can re-run this script at any time to refresh the docs:
786
+
787
+ ```bash
788
+ cd backtest-kit-project
789
+ node ./scripts/fetch_docs.mjs
790
+ ```
791
+
792
+ Or via the pre-configured npm script:
793
+
794
+ ```bash
795
+ npm run sync:lib
796
+ ```
797
+
708
798
  ## 🌍 Environment Variables
709
799
 
710
800
  Create a `.env` file in your project root:
package/build/index.cjs CHANGED
@@ -29,6 +29,7 @@ var BacktestKitGraph = require('@backtest-kit/graph');
29
29
  var BacktestKitOllama = require('@backtest-kit/ollama');
30
30
  var BacktestKitPinets = require('@backtest-kit/pinets');
31
31
  var BacktestKitSignals = require('@backtest-kit/signals');
32
+ var child_process = require('child_process');
32
33
 
33
34
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
34
35
  function _interopNamespaceDefault(e) {
@@ -284,15 +285,15 @@ const TYPES = {
284
285
 
285
286
  const entrySubject = new functoolsKit.BehaviorSubject();
286
287
 
287
- 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)));
288
- const __dirname$1 = path.dirname(__filename$1);
288
+ const __filename$2 = 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)));
289
+ const __dirname$2 = path.dirname(__filename$2);
289
290
  let _is_launched = false;
290
291
  class ResolveService {
291
292
  constructor() {
292
293
  this.loggerService = inject(TYPES.loggerService);
293
294
  this.loaderService = inject(TYPES.loaderService);
294
- this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname$1, '..', 'template');
295
- this.DEFAULT_MODULES_DIR = path.resolve(__dirname$1, '..', 'modules');
295
+ this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname$2, '..', 'template');
296
+ this.DEFAULT_MODULES_DIR = path.resolve(__dirname$2, '..', 'modules');
296
297
  this.OVERRIDE_TEMPLATE_DIR = path.resolve(process.cwd(), 'template');
297
298
  this.OVERRIDE_MODULES_DIR = path.resolve(process.cwd(), 'modules');
298
299
  this.getIsLaunched = () => {
@@ -454,6 +455,10 @@ const ALLOWED_EXTENSIONS = [
454
455
  `.js`,
455
456
  `.pine`,
456
457
  ];
458
+ const DISALLOWED_PATHS = [
459
+ "node_modules",
460
+ "@backtest-kit",
461
+ ];
457
462
  const getArgs = functoolsKit.singleshot(() => {
458
463
  const { values, positionals } = util.parseArgs({
459
464
  args: process.argv,
@@ -548,6 +553,10 @@ const getArgs = functoolsKit.singleshot(() => {
548
553
  type: "boolean",
549
554
  default: false,
550
555
  },
556
+ init: {
557
+ type: "boolean",
558
+ default: false,
559
+ },
551
560
  },
552
561
  strict: false,
553
562
  allowPositionals: true,
@@ -559,7 +568,9 @@ const getArgs = functoolsKit.singleshot(() => {
559
568
  });
560
569
  const getPositional = functoolsKit.singleshot(() => {
561
570
  const { positionals = [] } = getArgs();
562
- const result = positionals.find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
571
+ const result = positionals
572
+ .filter((value) => !DISALLOWED_PATHS.some((path) => value.includes(path)))
573
+ .find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
563
574
  return result || null;
564
575
  });
565
576
 
@@ -2220,7 +2231,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
2220
2231
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
2221
2232
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2222
2233
  });
2223
- const main$6 = async () => {
2234
+ const main$7 = async () => {
2224
2235
  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)))) {
2225
2236
  return;
2226
2237
  }
@@ -2231,7 +2242,7 @@ const main$6 = async () => {
2231
2242
  await cli.backtestMainService.connect();
2232
2243
  listenGracefulShutdown$4();
2233
2244
  };
2234
- main$6();
2245
+ main$7();
2235
2246
 
2236
2247
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
2237
2248
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -2252,7 +2263,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
2252
2263
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
2253
2264
  process.on("SIGINT", BEFORE_EXIT_FN$3);
2254
2265
  });
2255
- const main$5 = async () => {
2266
+ const main$6 = async () => {
2256
2267
  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)))) {
2257
2268
  return;
2258
2269
  }
@@ -2263,7 +2274,7 @@ const main$5 = async () => {
2263
2274
  cli.paperMainService.connect();
2264
2275
  listenGracefulShutdown$3();
2265
2276
  };
2266
- main$5();
2277
+ main$6();
2267
2278
 
2268
2279
  const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
2269
2280
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -2284,7 +2295,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
2284
2295
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
2285
2296
  process.on("SIGINT", BEFORE_EXIT_FN$2);
2286
2297
  });
2287
- const main$4 = async () => {
2298
+ const main$5 = async () => {
2288
2299
  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)))) {
2289
2300
  return;
2290
2301
  }
@@ -2295,7 +2306,7 @@ const main$4 = async () => {
2295
2306
  await cli.liveMainService.connect();
2296
2307
  listenGracefulShutdown$2();
2297
2308
  };
2298
- main$4();
2309
+ main$5();
2299
2310
 
2300
2311
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
2301
2312
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -2305,7 +2316,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
2305
2316
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
2306
2317
  process.on("SIGINT", BEFORE_EXIT_FN$1);
2307
2318
  });
2308
- const main$3 = async () => {
2319
+ const main$4 = async () => {
2309
2320
  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)))) {
2310
2321
  return;
2311
2322
  }
@@ -2315,7 +2326,7 @@ const main$3 = async () => {
2315
2326
  }
2316
2327
  listenGracefulShutdown$1();
2317
2328
  };
2318
- main$3();
2329
+ main$4();
2319
2330
 
2320
2331
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
2321
2332
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -2325,7 +2336,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
2325
2336
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
2326
2337
  process.on("SIGINT", BEFORE_EXIT_FN);
2327
2338
  });
2328
- const main$2 = async () => {
2339
+ const main$3 = async () => {
2329
2340
  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)))) {
2330
2341
  return;
2331
2342
  }
@@ -2335,7 +2346,7 @@ const main$2 = async () => {
2335
2346
  }
2336
2347
  listenGracefulShutdown();
2337
2348
  };
2338
- main$2();
2349
+ main$3();
2339
2350
 
2340
2351
  const EXTRACT_ROWS_FN = (plots, schema) => {
2341
2352
  const keys = Object.keys(schema);
@@ -2357,7 +2368,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
2357
2368
  }
2358
2369
  return rows;
2359
2370
  };
2360
- const main$1 = async () => {
2371
+ const main$2 = async () => {
2361
2372
  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)))) {
2362
2373
  return;
2363
2374
  }
@@ -2429,9 +2440,9 @@ const main$1 = async () => {
2429
2440
  }
2430
2441
  console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
2431
2442
  };
2432
- main$1();
2443
+ main$2();
2433
2444
 
2434
- const main = async () => {
2445
+ const main$1 = async () => {
2435
2446
  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)))) {
2436
2447
  return;
2437
2448
  }
@@ -2476,6 +2487,83 @@ const main = async () => {
2476
2487
  }
2477
2488
  console.log(JSON.stringify(candles, null, 2));
2478
2489
  };
2490
+ main$1();
2491
+
2492
+ 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
+ const __dirname$1 = path.dirname(__filename$1);
2494
+ const MUSTACHE_EXT = ".mustache";
2495
+ async function isDirEmpty(dirPath) {
2496
+ try {
2497
+ const files = await fs$1.readdir(dirPath);
2498
+ return files.length === 0;
2499
+ }
2500
+ catch (error) {
2501
+ if (error.code === "ENOENT") {
2502
+ return true;
2503
+ }
2504
+ throw error;
2505
+ }
2506
+ }
2507
+ async function copyDir(srcDir, destDir, data) {
2508
+ await fs$1.mkdir(destDir, { recursive: true });
2509
+ const entries = await fs$1.readdir(srcDir, { withFileTypes: true });
2510
+ for (const entry of entries) {
2511
+ const srcPath = path.join(srcDir, entry.name);
2512
+ if (entry.isDirectory()) {
2513
+ await copyDir(srcPath, path.join(destDir, entry.name), data);
2514
+ continue;
2515
+ }
2516
+ if (entry.name.endsWith(MUSTACHE_EXT)) {
2517
+ const destName = entry.name.slice(0, -MUSTACHE_EXT.length);
2518
+ const destPath = path.join(destDir, destName);
2519
+ const template = await fs$1.readFile(srcPath, "utf-8");
2520
+ const rendered = Mustache.render(template, data);
2521
+ await fs$1.writeFile(destPath, rendered, "utf-8");
2522
+ console.log(` -> ${destPath}`);
2523
+ }
2524
+ else {
2525
+ const destPath = path.join(destDir, entry.name);
2526
+ await fs$1.copyFile(srcPath, destPath);
2527
+ console.log(` -> ${destPath}`);
2528
+ }
2529
+ }
2530
+ }
2531
+ function runScript(scriptPath, cwd) {
2532
+ return new Promise((resolve, reject) => {
2533
+ const node = process.execPath;
2534
+ const child = child_process.spawn(node, [scriptPath], { cwd, stdio: "inherit" });
2535
+ child.on("close", (code) => {
2536
+ if (code !== 0) {
2537
+ reject(new Error(`Script exited with code ${code}`));
2538
+ return;
2539
+ }
2540
+ resolve();
2541
+ });
2542
+ child.on("error", reject);
2543
+ });
2544
+ }
2545
+ const main = async () => {
2546
+ 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
+ return;
2548
+ }
2549
+ const { values } = getArgs();
2550
+ if (!values.init) {
2551
+ return;
2552
+ }
2553
+ const projectName = values.output || "backtest-kit-project";
2554
+ const projectPath = path.join(process.cwd(), projectName);
2555
+ const templatePath = path.join(__dirname$1, "../../template/project");
2556
+ const isEmpty = await isDirEmpty(projectPath);
2557
+ if (!isEmpty) {
2558
+ console.error(`Directory "${projectName}" already exists and is not empty.`);
2559
+ process.exit(1);
2560
+ }
2561
+ console.log(`Creating project in ${projectPath}`);
2562
+ await copyDir(templatePath, projectPath, { PROJECT_NAME: projectName });
2563
+ console.log(`Fetching docs...`);
2564
+ await runScript(path.join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
2565
+ console.log(`Done! Project created at ${projectPath}`);
2566
+ };
2479
2567
  main();
2480
2568
 
2481
2569
  function setLogger(logger) {
package/build/index.mjs CHANGED
@@ -4,8 +4,8 @@ import { Storage, Notification, Markdown, Report, Dump, Memory, StorageLive, Sto
4
4
  import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose, execpool, queued, sleep, randomString, createAwaiter, TIMEOUT_SYMBOL, typo, retry, trycatch, memoize } from 'functools-kit';
5
5
  import fs, { constants } from 'fs';
6
6
  import * as stackTrace from 'stack-trace';
7
- import path, { basename, extname, join, resolve } from 'path';
8
- import fs$1, { access, readFile, mkdir, writeFile } from 'fs/promises';
7
+ import path, { basename, extname, join, resolve, dirname } from 'path';
8
+ import fs$1, { access, readFile, mkdir, writeFile, readdir, copyFile } from 'fs/promises';
9
9
  import dotenv from 'dotenv';
10
10
  import { createActivator } from 'di-kit';
11
11
  import { fileURLToPath } from 'url';
@@ -30,6 +30,7 @@ import * as BacktestKitOllama from '@backtest-kit/ollama';
30
30
  import * as BacktestKitPinets from '@backtest-kit/pinets';
31
31
  import { run as run$1, Code, toMarkdown } from '@backtest-kit/pinets';
32
32
  import * as BacktestKitSignals from '@backtest-kit/signals';
33
+ import { spawn } from 'child_process';
33
34
 
34
35
  /**
35
36
  * Fix for `Attempted to assign to readonly property (at redactToken)`
@@ -259,15 +260,15 @@ const TYPES = {
259
260
 
260
261
  const entrySubject = new BehaviorSubject();
261
262
 
262
- const __filename = fileURLToPath(import.meta.url);
263
- const __dirname = path.dirname(__filename);
263
+ const __filename$1 = fileURLToPath(import.meta.url);
264
+ const __dirname$1 = path.dirname(__filename$1);
264
265
  let _is_launched = false;
265
266
  class ResolveService {
266
267
  constructor() {
267
268
  this.loggerService = inject(TYPES.loggerService);
268
269
  this.loaderService = inject(TYPES.loaderService);
269
- this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname, '..', 'template');
270
- this.DEFAULT_MODULES_DIR = path.resolve(__dirname, '..', 'modules');
270
+ this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname$1, '..', 'template');
271
+ this.DEFAULT_MODULES_DIR = path.resolve(__dirname$1, '..', 'modules');
271
272
  this.OVERRIDE_TEMPLATE_DIR = path.resolve(process.cwd(), 'template');
272
273
  this.OVERRIDE_MODULES_DIR = path.resolve(process.cwd(), 'modules');
273
274
  this.getIsLaunched = () => {
@@ -429,6 +430,10 @@ const ALLOWED_EXTENSIONS = [
429
430
  `.js`,
430
431
  `.pine`,
431
432
  ];
433
+ const DISALLOWED_PATHS = [
434
+ "node_modules",
435
+ "@backtest-kit",
436
+ ];
432
437
  const getArgs = singleshot(() => {
433
438
  const { values, positionals } = parseArgs({
434
439
  args: process.argv,
@@ -523,6 +528,10 @@ const getArgs = singleshot(() => {
523
528
  type: "boolean",
524
529
  default: false,
525
530
  },
531
+ init: {
532
+ type: "boolean",
533
+ default: false,
534
+ },
526
535
  },
527
536
  strict: false,
528
537
  allowPositionals: true,
@@ -534,7 +543,9 @@ const getArgs = singleshot(() => {
534
543
  });
535
544
  const getPositional = singleshot(() => {
536
545
  const { positionals = [] } = getArgs();
537
- const result = positionals.find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
546
+ const result = positionals
547
+ .filter((value) => !DISALLOWED_PATHS.some((path) => value.includes(path)))
548
+ .find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
538
549
  return result || null;
539
550
  });
540
551
 
@@ -2191,7 +2202,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
2191
2202
  const listenGracefulShutdown$4 = singleshot(() => {
2192
2203
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2193
2204
  });
2194
- const main$6 = async () => {
2205
+ const main$7 = async () => {
2195
2206
  if (!getEntry(import.meta.url)) {
2196
2207
  return;
2197
2208
  }
@@ -2202,7 +2213,7 @@ const main$6 = async () => {
2202
2213
  await cli.backtestMainService.connect();
2203
2214
  listenGracefulShutdown$4();
2204
2215
  };
2205
- main$6();
2216
+ main$7();
2206
2217
 
2207
2218
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
2208
2219
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -2223,7 +2234,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
2223
2234
  const listenGracefulShutdown$3 = singleshot(() => {
2224
2235
  process.on("SIGINT", BEFORE_EXIT_FN$3);
2225
2236
  });
2226
- const main$5 = async () => {
2237
+ const main$6 = async () => {
2227
2238
  if (!getEntry(import.meta.url)) {
2228
2239
  return;
2229
2240
  }
@@ -2234,7 +2245,7 @@ const main$5 = async () => {
2234
2245
  cli.paperMainService.connect();
2235
2246
  listenGracefulShutdown$3();
2236
2247
  };
2237
- main$5();
2248
+ main$6();
2238
2249
 
2239
2250
  const BEFORE_EXIT_FN$2 = singleshot(async () => {
2240
2251
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -2255,7 +2266,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
2255
2266
  const listenGracefulShutdown$2 = singleshot(() => {
2256
2267
  process.on("SIGINT", BEFORE_EXIT_FN$2);
2257
2268
  });
2258
- const main$4 = async () => {
2269
+ const main$5 = async () => {
2259
2270
  if (!getEntry(import.meta.url)) {
2260
2271
  return;
2261
2272
  }
@@ -2266,7 +2277,7 @@ const main$4 = async () => {
2266
2277
  await cli.liveMainService.connect();
2267
2278
  listenGracefulShutdown$2();
2268
2279
  };
2269
- main$4();
2280
+ main$5();
2270
2281
 
2271
2282
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
2272
2283
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -2276,7 +2287,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
2276
2287
  const listenGracefulShutdown$1 = singleshot(() => {
2277
2288
  process.on("SIGINT", BEFORE_EXIT_FN$1);
2278
2289
  });
2279
- const main$3 = async () => {
2290
+ const main$4 = async () => {
2280
2291
  if (!getEntry(import.meta.url)) {
2281
2292
  return;
2282
2293
  }
@@ -2286,7 +2297,7 @@ const main$3 = async () => {
2286
2297
  }
2287
2298
  listenGracefulShutdown$1();
2288
2299
  };
2289
- main$3();
2300
+ main$4();
2290
2301
 
2291
2302
  const BEFORE_EXIT_FN = singleshot(async () => {
2292
2303
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -2296,7 +2307,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
2296
2307
  const listenGracefulShutdown = singleshot(() => {
2297
2308
  process.on("SIGINT", BEFORE_EXIT_FN);
2298
2309
  });
2299
- const main$2 = async () => {
2310
+ const main$3 = async () => {
2300
2311
  if (!getEntry(import.meta.url)) {
2301
2312
  return;
2302
2313
  }
@@ -2306,7 +2317,7 @@ const main$2 = async () => {
2306
2317
  }
2307
2318
  listenGracefulShutdown();
2308
2319
  };
2309
- main$2();
2320
+ main$3();
2310
2321
 
2311
2322
  const EXTRACT_ROWS_FN = (plots, schema) => {
2312
2323
  const keys = Object.keys(schema);
@@ -2328,7 +2339,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
2328
2339
  }
2329
2340
  return rows;
2330
2341
  };
2331
- const main$1 = async () => {
2342
+ const main$2 = async () => {
2332
2343
  if (!getEntry(import.meta.url)) {
2333
2344
  return;
2334
2345
  }
@@ -2400,9 +2411,9 @@ const main$1 = async () => {
2400
2411
  }
2401
2412
  console.log(await toMarkdown(signalId, plots, signalSchema));
2402
2413
  };
2403
- main$1();
2414
+ main$2();
2404
2415
 
2405
- const main = async () => {
2416
+ const main$1 = async () => {
2406
2417
  if (!getEntry(import.meta.url)) {
2407
2418
  return;
2408
2419
  }
@@ -2447,6 +2458,83 @@ const main = async () => {
2447
2458
  }
2448
2459
  console.log(JSON.stringify(candles, null, 2));
2449
2460
  };
2461
+ main$1();
2462
+
2463
+ const __filename = fileURLToPath(import.meta.url);
2464
+ const __dirname = dirname(__filename);
2465
+ const MUSTACHE_EXT = ".mustache";
2466
+ async function isDirEmpty(dirPath) {
2467
+ try {
2468
+ const files = await readdir(dirPath);
2469
+ return files.length === 0;
2470
+ }
2471
+ catch (error) {
2472
+ if (error.code === "ENOENT") {
2473
+ return true;
2474
+ }
2475
+ throw error;
2476
+ }
2477
+ }
2478
+ async function copyDir(srcDir, destDir, data) {
2479
+ await mkdir(destDir, { recursive: true });
2480
+ const entries = await readdir(srcDir, { withFileTypes: true });
2481
+ for (const entry of entries) {
2482
+ const srcPath = join(srcDir, entry.name);
2483
+ if (entry.isDirectory()) {
2484
+ await copyDir(srcPath, join(destDir, entry.name), data);
2485
+ continue;
2486
+ }
2487
+ if (entry.name.endsWith(MUSTACHE_EXT)) {
2488
+ const destName = entry.name.slice(0, -MUSTACHE_EXT.length);
2489
+ const destPath = join(destDir, destName);
2490
+ const template = await readFile(srcPath, "utf-8");
2491
+ const rendered = Mustache.render(template, data);
2492
+ await writeFile(destPath, rendered, "utf-8");
2493
+ console.log(` -> ${destPath}`);
2494
+ }
2495
+ else {
2496
+ const destPath = join(destDir, entry.name);
2497
+ await copyFile(srcPath, destPath);
2498
+ console.log(` -> ${destPath}`);
2499
+ }
2500
+ }
2501
+ }
2502
+ function runScript(scriptPath, cwd) {
2503
+ return new Promise((resolve, reject) => {
2504
+ const node = process.execPath;
2505
+ const child = spawn(node, [scriptPath], { cwd, stdio: "inherit" });
2506
+ child.on("close", (code) => {
2507
+ if (code !== 0) {
2508
+ reject(new Error(`Script exited with code ${code}`));
2509
+ return;
2510
+ }
2511
+ resolve();
2512
+ });
2513
+ child.on("error", reject);
2514
+ });
2515
+ }
2516
+ const main = async () => {
2517
+ if (!getEntry(import.meta.url)) {
2518
+ return;
2519
+ }
2520
+ const { values } = getArgs();
2521
+ if (!values.init) {
2522
+ return;
2523
+ }
2524
+ const projectName = values.output || "backtest-kit-project";
2525
+ const projectPath = join(process.cwd(), projectName);
2526
+ const templatePath = join(__dirname, "../../template/project");
2527
+ const isEmpty = await isDirEmpty(projectPath);
2528
+ if (!isEmpty) {
2529
+ console.error(`Directory "${projectName}" already exists and is not empty.`);
2530
+ process.exit(1);
2531
+ }
2532
+ console.log(`Creating project in ${projectPath}`);
2533
+ await copyDir(templatePath, projectPath, { PROJECT_NAME: projectName });
2534
+ console.log(`Fetching docs...`);
2535
+ await runScript(join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
2536
+ console.log(`Done! Project created at ${projectPath}`);
2537
+ };
2450
2538
  main();
2451
2539
 
2452
2540
  function setLogger(logger) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/cli",
3
- "version": "5.11.0",
3
+ "version": "5.11.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",