@backtest-kit/cli 5.10.2 → 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) {
@@ -118,6 +119,7 @@ BacktestKit.setConfig({
118
119
  BacktestKit.setConfig({
119
120
  CC_ENABLE_DCA_EVERYWHERE: true,
120
121
  CC_ENABLE_PPPL_EVERYWHERE: true,
122
+ CC_ENABLE_TRAILING_EVERYWHERE: true,
121
123
  });
122
124
  BacktestKit.setConfig({
123
125
  CC_MAX_SIGNAL_GENERATION_SECONDS: 15 * 60,
@@ -283,15 +285,15 @@ const TYPES = {
283
285
 
284
286
  const entrySubject = new functoolsKit.BehaviorSubject();
285
287
 
286
- 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)));
287
- 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);
288
290
  let _is_launched = false;
289
291
  class ResolveService {
290
292
  constructor() {
291
293
  this.loggerService = inject(TYPES.loggerService);
292
294
  this.loaderService = inject(TYPES.loaderService);
293
- this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname$1, '..', 'template');
294
- 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');
295
297
  this.OVERRIDE_TEMPLATE_DIR = path.resolve(process.cwd(), 'template');
296
298
  this.OVERRIDE_MODULES_DIR = path.resolve(process.cwd(), 'modules');
297
299
  this.getIsLaunched = () => {
@@ -445,6 +447,18 @@ var FrameName;
445
447
  })(FrameName || (FrameName = {}));
446
448
  var FrameName$1 = FrameName;
447
449
 
450
+ const ALLOWED_EXTENSIONS = [
451
+ `.cjs`,
452
+ `.mjs`,
453
+ `.ts`,
454
+ `.tsx`,
455
+ `.js`,
456
+ `.pine`,
457
+ ];
458
+ const DISALLOWED_PATHS = [
459
+ "node_modules",
460
+ "@backtest-kit",
461
+ ];
448
462
  const getArgs = functoolsKit.singleshot(() => {
449
463
  const { values, positionals } = util.parseArgs({
450
464
  args: process.argv,
@@ -539,6 +553,10 @@ const getArgs = functoolsKit.singleshot(() => {
539
553
  type: "boolean",
540
554
  default: false,
541
555
  },
556
+ init: {
557
+ type: "boolean",
558
+ default: false,
559
+ },
542
560
  },
543
561
  strict: false,
544
562
  allowPositionals: true,
@@ -548,6 +566,13 @@ const getArgs = functoolsKit.singleshot(() => {
548
566
  positionals,
549
567
  };
550
568
  });
569
+ const getPositional = functoolsKit.singleshot(() => {
570
+ const { positionals = [] } = getArgs();
571
+ const result = positionals
572
+ .filter((value) => !DISALLOWED_PATHS.some((path) => value.includes(path)))
573
+ .find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
574
+ return result || null;
575
+ });
551
576
 
552
577
  const ADD_FRAME_FN = (self) => {
553
578
  self.loggerService.log("Adding February 2024 as a default frame schema");
@@ -733,11 +758,11 @@ class BacktestMainService {
733
758
  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)))) {
734
759
  return;
735
760
  }
736
- const { values, positionals } = getArgs();
761
+ const { values } = getArgs();
737
762
  if (!values.backtest) {
738
763
  return;
739
764
  }
740
- const [entryPoint = null] = positionals.slice(-1);
765
+ const entryPoint = getPositional();
741
766
  if (!entryPoint) {
742
767
  throw new Error("Entry point is required");
743
768
  }
@@ -812,11 +837,11 @@ class LiveMainService {
812
837
  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)))) {
813
838
  return;
814
839
  }
815
- const { values, positionals } = getArgs();
840
+ const { values } = getArgs();
816
841
  if (!values.live) {
817
842
  return;
818
843
  }
819
- const [entryPoint = null] = positionals.slice(-1);
844
+ const entryPoint = getPositional();
820
845
  if (!entryPoint) {
821
846
  throw new Error("Entry point is required");
822
847
  }
@@ -885,11 +910,11 @@ class PaperMainService {
885
910
  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)))) {
886
911
  return;
887
912
  }
888
- const { values, positionals } = getArgs();
913
+ const { values } = getArgs();
889
914
  if (!values.paper) {
890
915
  return;
891
916
  }
892
- const [entryPoint = null] = positionals.slice(-1);
917
+ const entryPoint = getPositional();
893
918
  if (!entryPoint) {
894
919
  throw new Error("Entry point is required");
895
920
  }
@@ -2206,7 +2231,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
2206
2231
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
2207
2232
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2208
2233
  });
2209
- const main$6 = async () => {
2234
+ const main$7 = async () => {
2210
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)))) {
2211
2236
  return;
2212
2237
  }
@@ -2217,7 +2242,7 @@ const main$6 = async () => {
2217
2242
  await cli.backtestMainService.connect();
2218
2243
  listenGracefulShutdown$4();
2219
2244
  };
2220
- main$6();
2245
+ main$7();
2221
2246
 
2222
2247
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
2223
2248
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -2238,7 +2263,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
2238
2263
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
2239
2264
  process.on("SIGINT", BEFORE_EXIT_FN$3);
2240
2265
  });
2241
- const main$5 = async () => {
2266
+ const main$6 = async () => {
2242
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)))) {
2243
2268
  return;
2244
2269
  }
@@ -2249,7 +2274,7 @@ const main$5 = async () => {
2249
2274
  cli.paperMainService.connect();
2250
2275
  listenGracefulShutdown$3();
2251
2276
  };
2252
- main$5();
2277
+ main$6();
2253
2278
 
2254
2279
  const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
2255
2280
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -2270,7 +2295,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
2270
2295
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
2271
2296
  process.on("SIGINT", BEFORE_EXIT_FN$2);
2272
2297
  });
2273
- const main$4 = async () => {
2298
+ const main$5 = async () => {
2274
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)))) {
2275
2300
  return;
2276
2301
  }
@@ -2281,7 +2306,7 @@ const main$4 = async () => {
2281
2306
  await cli.liveMainService.connect();
2282
2307
  listenGracefulShutdown$2();
2283
2308
  };
2284
- main$4();
2309
+ main$5();
2285
2310
 
2286
2311
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
2287
2312
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -2291,7 +2316,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
2291
2316
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
2292
2317
  process.on("SIGINT", BEFORE_EXIT_FN$1);
2293
2318
  });
2294
- const main$3 = async () => {
2319
+ const main$4 = async () => {
2295
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)))) {
2296
2321
  return;
2297
2322
  }
@@ -2301,7 +2326,7 @@ const main$3 = async () => {
2301
2326
  }
2302
2327
  listenGracefulShutdown$1();
2303
2328
  };
2304
- main$3();
2329
+ main$4();
2305
2330
 
2306
2331
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
2307
2332
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -2311,7 +2336,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
2311
2336
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
2312
2337
  process.on("SIGINT", BEFORE_EXIT_FN);
2313
2338
  });
2314
- const main$2 = async () => {
2339
+ const main$3 = async () => {
2315
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)))) {
2316
2341
  return;
2317
2342
  }
@@ -2321,7 +2346,7 @@ const main$2 = async () => {
2321
2346
  }
2322
2347
  listenGracefulShutdown();
2323
2348
  };
2324
- main$2();
2349
+ main$3();
2325
2350
 
2326
2351
  const EXTRACT_ROWS_FN = (plots, schema) => {
2327
2352
  const keys = Object.keys(schema);
@@ -2343,15 +2368,15 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
2343
2368
  }
2344
2369
  return rows;
2345
2370
  };
2346
- const main$1 = async () => {
2371
+ const main$2 = async () => {
2347
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)))) {
2348
2373
  return;
2349
2374
  }
2350
- const { values, positionals } = getArgs();
2375
+ const { values } = getArgs();
2351
2376
  if (!values.pine) {
2352
2377
  return;
2353
2378
  }
2354
- const [entryPoint = null] = positionals.slice(-1);
2379
+ const entryPoint = getPositional();
2355
2380
  if (!entryPoint) {
2356
2381
  return;
2357
2382
  }
@@ -2415,9 +2440,9 @@ const main$1 = async () => {
2415
2440
  }
2416
2441
  console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
2417
2442
  };
2418
- main$1();
2443
+ main$2();
2419
2444
 
2420
- const main = async () => {
2445
+ const main$1 = async () => {
2421
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)))) {
2422
2447
  return;
2423
2448
  }
@@ -2462,6 +2487,83 @@ const main = async () => {
2462
2487
  }
2463
2488
  console.log(JSON.stringify(candles, null, 2));
2464
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
+ };
2465
2567
  main();
2466
2568
 
2467
2569
  function setLogger(logger) {