@backtest-kit/cli 8.2.0 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1369,6 +1369,48 @@ Or via the pre-configured npm script:
1369
1369
  npm run sync:lib
1370
1370
  ```
1371
1371
 
1372
+ ## 🐳 Running in Docker (`--docker`)
1373
+
1374
+ CLI can scaffold a ready-to-use Docker workspace: self-contained directory with `docker-compose.yaml` and a strategy entry point.
1375
+
1376
+ ### CLI Flags
1377
+
1378
+ | Flag | Type | Description |
1379
+ |------|------|-------------|
1380
+ | `--docker` | boolean | Scaffold a Docker workspace |
1381
+ | `--output` | string | Target directory name (default: `backtest-kit-docker`) |
1382
+
1383
+ ### Usage
1384
+
1385
+ ```bash
1386
+ npx @backtest-kit/cli --docker
1387
+ ```
1388
+
1389
+ Creates `./backtest-kit-docker/` in the current working directory.
1390
+
1391
+ Override the directory name with `--output`:
1392
+
1393
+ ```bash
1394
+ npx @backtest-kit/cli --docker --output my-docker-workspace
1395
+ ```
1396
+
1397
+ The target directory must not exist or must be empty — the command aborts if it contains any files.
1398
+
1399
+ ### Generated Workspace Structure
1400
+
1401
+ ```
1402
+ backtest-kit-docker/
1403
+ ├── docker-compose.yaml # ready-to-run service definition
1404
+ ├── .env.example # environment variable reference
1405
+ ├── package.json # dependencies for editing strategies locally
1406
+ ├── tsconfig.json # TypeScript config for content/
1407
+ └── content/
1408
+ └── feb_2026/
1409
+ ├── feb_2026.strategy.ts # example strategy entry point
1410
+ └── modules/
1411
+ └── backtest.module.ts # CCXT Binance exchange + frame schema
1412
+ ```
1413
+
1372
1414
  ## 🌍 Environment Variables
1373
1415
 
1374
1416
  Create a `.env` file in your project root:
package/build/index.cjs CHANGED
@@ -266,16 +266,16 @@ const TYPES = {
266
266
 
267
267
  const entrySubject = new functoolsKit.BehaviorSubject();
268
268
 
269
- 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)));
270
- const __dirname$2 = path.dirname(__filename$2);
269
+ const __filename$3 = 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)));
270
+ const __dirname$3 = path.dirname(__filename$3);
271
271
  let _is_launched = false;
272
272
  class ResolveService {
273
273
  constructor() {
274
274
  this.loggerService = inject(TYPES.loggerService);
275
275
  this.loaderService = inject(TYPES.loaderService);
276
- this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname$2, '..', 'template');
277
- this.DEFAULT_MODULES_DIR = path.resolve(__dirname$2, '..', 'modules');
278
- this.DEFAULT_CONFIG_DIR = path.resolve(__dirname$2, '..', 'config');
276
+ this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname$3, '..', 'template');
277
+ this.DEFAULT_MODULES_DIR = path.resolve(__dirname$3, '..', 'modules');
278
+ this.DEFAULT_CONFIG_DIR = path.resolve(__dirname$3, '..', 'config');
279
279
  this.OVERRIDE_TEMPLATE_DIR = path.resolve(process.cwd(), 'template');
280
280
  this.OVERRIDE_MODULES_DIR = path.resolve(process.cwd(), 'modules');
281
281
  this.OVERRIDE_CONFIG_DIR = path.resolve(process.cwd(), 'config');
@@ -598,6 +598,10 @@ const getArgs = functoolsKit.singleshot(() => {
598
598
  type: "boolean",
599
599
  default: false,
600
600
  },
601
+ docker: {
602
+ type: "boolean",
603
+ default: false,
604
+ },
601
605
  help: {
602
606
  type: "boolean",
603
607
  default: false,
@@ -3007,14 +3011,14 @@ const cli = {
3007
3011
  };
3008
3012
  init();
3009
3013
 
3010
- const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "brokerdebug", "flush", "init", "help", "version"];
3014
+ const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "brokerdebug", "flush", "init", "docker", "help", "version"];
3011
3015
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3012
3016
  const HELP_TEXT$1 = `
3013
3017
  Example:
3014
3018
 
3015
3019
  node ${ENTRY_PATH$1} --help
3016
3020
  `.trimStart();
3017
- const main$e = async () => {
3021
+ const main$f = async () => {
3018
3022
  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)))) {
3019
3023
  return;
3020
3024
  }
@@ -3022,14 +3026,14 @@ const main$e = async () => {
3022
3026
  if (MODES.some((mode) => values[mode])) {
3023
3027
  return;
3024
3028
  }
3025
- process.stdout.write(`@backtest-kit/cli ${"8.2.0"}\n`);
3029
+ process.stdout.write(`@backtest-kit/cli ${"8.3.0"}\n`);
3026
3030
  process.stdout.write("\n");
3027
3031
  process.stdout.write(`Run with --help to see available commands.\n`);
3028
3032
  process.stdout.write("\n");
3029
3033
  process.stdout.write(HELP_TEXT$1);
3030
3034
  process.exit(0);
3031
3035
  };
3032
- main$e();
3036
+ main$f();
3033
3037
 
3034
3038
  const notifyShutdown = functoolsKit.singleshot(async () => {
3035
3039
  console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
@@ -3045,7 +3049,7 @@ const flush = async (entryPoint) => {
3045
3049
  console.log(`Removed: ${target}`);
3046
3050
  }
3047
3051
  };
3048
- const main$d = async () => {
3052
+ const main$e = async () => {
3049
3053
  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)))) {
3050
3054
  return;
3051
3055
  }
@@ -3062,7 +3066,7 @@ const main$d = async () => {
3062
3066
  }
3063
3067
  process.exit(0);
3064
3068
  };
3065
- main$d();
3069
+ main$e();
3066
3070
 
3067
3071
  const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3068
3072
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3084,7 +3088,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3084
3088
  const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
3085
3089
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3086
3090
  });
3087
- const main$c = async () => {
3091
+ const main$d = async () => {
3088
3092
  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)))) {
3089
3093
  return;
3090
3094
  }
@@ -3099,7 +3103,7 @@ const main$c = async () => {
3099
3103
  await cli.backtestMainService.connect();
3100
3104
  listenGracefulShutdown$5();
3101
3105
  };
3102
- main$c();
3106
+ main$d();
3103
3107
 
3104
3108
  const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3105
3109
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3117,7 +3121,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3117
3121
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
3118
3122
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3119
3123
  });
3120
- const main$b = async () => {
3124
+ const main$c = async () => {
3121
3125
  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)))) {
3122
3126
  return;
3123
3127
  }
@@ -3133,7 +3137,7 @@ const main$b = async () => {
3133
3137
  listenGracefulShutdown$4();
3134
3138
  await cli.walkerMainService.connect();
3135
3139
  };
3136
- main$b();
3140
+ main$c();
3137
3141
 
3138
3142
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3139
3143
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3154,7 +3158,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3154
3158
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
3155
3159
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3156
3160
  });
3157
- const main$a = async () => {
3161
+ const main$b = async () => {
3158
3162
  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)))) {
3159
3163
  return;
3160
3164
  }
@@ -3165,7 +3169,7 @@ const main$a = async () => {
3165
3169
  cli.paperMainService.connect();
3166
3170
  listenGracefulShutdown$3();
3167
3171
  };
3168
- main$a();
3172
+ main$b();
3169
3173
 
3170
3174
  const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
3171
3175
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -3186,7 +3190,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
3186
3190
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
3187
3191
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3188
3192
  });
3189
- const main$9 = async () => {
3193
+ const main$a = async () => {
3190
3194
  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)))) {
3191
3195
  return;
3192
3196
  }
@@ -3197,7 +3201,7 @@ const main$9 = async () => {
3197
3201
  await cli.liveMainService.connect();
3198
3202
  listenGracefulShutdown$2();
3199
3203
  };
3200
- main$9();
3204
+ main$a();
3201
3205
 
3202
3206
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
3203
3207
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -3207,7 +3211,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
3207
3211
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
3208
3212
  process.on("SIGINT", BEFORE_EXIT_FN$1);
3209
3213
  });
3210
- const main$8 = async () => {
3214
+ const main$9 = async () => {
3211
3215
  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)))) {
3212
3216
  return;
3213
3217
  }
@@ -3217,7 +3221,7 @@ const main$8 = async () => {
3217
3221
  }
3218
3222
  listenGracefulShutdown$1();
3219
3223
  };
3220
- main$8();
3224
+ main$9();
3221
3225
 
3222
3226
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
3223
3227
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -3227,7 +3231,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
3227
3231
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
3228
3232
  process.on("SIGINT", BEFORE_EXIT_FN);
3229
3233
  });
3230
- const main$7 = async () => {
3234
+ const main$8 = async () => {
3231
3235
  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)))) {
3232
3236
  return;
3233
3237
  }
@@ -3237,7 +3241,7 @@ const main$7 = async () => {
3237
3241
  }
3238
3242
  listenGracefulShutdown();
3239
3243
  };
3240
- main$7();
3244
+ main$8();
3241
3245
 
3242
3246
  const EXTRACT_ROWS_FN = (plots, schema) => {
3243
3247
  const keys = Object.keys(schema);
@@ -3259,7 +3263,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
3259
3263
  }
3260
3264
  return rows;
3261
3265
  };
3262
- const main$6 = async () => {
3266
+ const main$7 = async () => {
3263
3267
  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)))) {
3264
3268
  return;
3265
3269
  }
@@ -3339,9 +3343,9 @@ const main$6 = async () => {
3339
3343
  console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
3340
3344
  process.exit(0);
3341
3345
  };
3342
- main$6();
3346
+ main$7();
3343
3347
 
3344
- const main$5 = async () => {
3348
+ const main$6 = async () => {
3345
3349
  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)))) {
3346
3350
  return;
3347
3351
  }
@@ -3376,9 +3380,9 @@ const main$5 = async () => {
3376
3380
  };
3377
3381
  process.on("SIGINT", beforeExit);
3378
3382
  };
3379
- main$5();
3383
+ main$6();
3380
3384
 
3381
- const main$4 = async () => {
3385
+ const main$5 = async () => {
3382
3386
  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)))) {
3383
3387
  return;
3384
3388
  }
@@ -3439,9 +3443,9 @@ const main$4 = async () => {
3439
3443
  console.log(JSON.stringify(candles, null, 2));
3440
3444
  process.exit(0);
3441
3445
  };
3442
- main$4();
3446
+ main$5();
3443
3447
 
3444
- const main$3 = async () => {
3448
+ const main$4 = async () => {
3445
3449
  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)))) {
3446
3450
  return;
3447
3451
  }
@@ -3535,16 +3539,16 @@ const main$3 = async () => {
3535
3539
  }
3536
3540
  process.exit(0);
3537
3541
  };
3538
- main$3();
3542
+ main$4();
3539
3543
 
3540
- 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)));
3541
- const __dirname$1 = path.dirname(__filename$1);
3544
+ 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)));
3545
+ const __dirname$2 = path.dirname(__filename$2);
3542
3546
  const MUSTACHE_EXT = ".mustache";
3543
3547
  const MUSTACHE_RENAME = {
3544
3548
  "gitignore": ".gitignore",
3545
3549
  "package": "package.json",
3546
3550
  };
3547
- async function isDirEmpty(dirPath) {
3551
+ async function isDirEmpty$1(dirPath) {
3548
3552
  try {
3549
3553
  const files = await fs$1.readdir(dirPath);
3550
3554
  return files.length === 0;
@@ -3556,13 +3560,13 @@ async function isDirEmpty(dirPath) {
3556
3560
  throw error;
3557
3561
  }
3558
3562
  }
3559
- async function copyDir(srcDir, destDir, data) {
3563
+ async function copyDir$1(srcDir, destDir, data) {
3560
3564
  await fs$1.mkdir(destDir, { recursive: true });
3561
3565
  const entries = await fs$1.readdir(srcDir, { withFileTypes: true });
3562
3566
  for (const entry of entries) {
3563
3567
  const srcPath = path.join(srcDir, entry.name);
3564
3568
  if (entry.isDirectory()) {
3565
- await copyDir(srcPath, path.join(destDir, entry.name), data);
3569
+ await copyDir$1(srcPath, path.join(destDir, entry.name), data);
3566
3570
  continue;
3567
3571
  }
3568
3572
  if (entry.name === ".gitkeep") {
@@ -3598,7 +3602,7 @@ function runScript(scriptPath, cwd) {
3598
3602
  child.on("error", reject);
3599
3603
  });
3600
3604
  }
3601
- function runNpmInstall(cwd) {
3605
+ function runNpmInstall$1(cwd) {
3602
3606
  return new Promise((resolve, reject) => {
3603
3607
  const npm = process.platform === "win32" ? "npm.cmd" : "npm";
3604
3608
  const child = child_process.spawn(npm, ["install"], { cwd, stdio: "inherit", shell: true });
@@ -3612,7 +3616,7 @@ function runNpmInstall(cwd) {
3612
3616
  child.on("error", reject);
3613
3617
  });
3614
3618
  }
3615
- const main$2 = async () => {
3619
+ const main$3 = async () => {
3616
3620
  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)))) {
3617
3621
  return;
3618
3622
  }
@@ -3622,21 +3626,95 @@ const main$2 = async () => {
3622
3626
  }
3623
3627
  const projectName = values.output || "backtest-kit-project";
3624
3628
  const projectPath = path.join(process.cwd(), projectName);
3625
- const templatePath = path.join(__dirname$1, "../template/project");
3626
- const isEmpty = await isDirEmpty(projectPath);
3629
+ const templatePath = path.join(__dirname$2, "../template/project");
3630
+ const isEmpty = await isDirEmpty$1(projectPath);
3627
3631
  if (!isEmpty) {
3628
3632
  console.error(`Directory "${projectName}" already exists and is not empty.`);
3629
3633
  process.exit(1);
3630
3634
  }
3631
3635
  console.log(`Creating project in ${projectPath}`);
3632
- await copyDir(templatePath, projectPath, { PROJECT_NAME: projectName });
3636
+ await copyDir$1(templatePath, projectPath, { PROJECT_NAME: projectName });
3633
3637
  console.log(`Fetching docs...`);
3634
3638
  await runScript(path.join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
3635
3639
  console.log(`Installing dependencies...`);
3636
- await runNpmInstall(projectPath);
3640
+ await runNpmInstall$1(projectPath);
3637
3641
  console.log(`Done! Project created at ${projectPath}`);
3638
3642
  process.exit(0);
3639
3643
  };
3644
+ main$3();
3645
+
3646
+ 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)));
3647
+ const __dirname$1 = path.dirname(__filename$1);
3648
+ async function isDirEmpty(dirPath) {
3649
+ try {
3650
+ const files = await fs$1.readdir(dirPath);
3651
+ return files.length === 0;
3652
+ }
3653
+ catch (error) {
3654
+ if (error.code === "ENOENT") {
3655
+ return true;
3656
+ }
3657
+ throw error;
3658
+ }
3659
+ }
3660
+ async function copyDir(srcDir, destDir) {
3661
+ await fs$1.mkdir(destDir, { recursive: true });
3662
+ const entries = await fs$1.readdir(srcDir, { withFileTypes: true });
3663
+ for (const entry of entries) {
3664
+ const srcPath = path.join(srcDir, entry.name);
3665
+ if (entry.isDirectory()) {
3666
+ await copyDir(srcPath, path.join(destDir, entry.name));
3667
+ continue;
3668
+ }
3669
+ if (entry.name === ".gitkeep") {
3670
+ continue;
3671
+ }
3672
+ const destPath = path.join(destDir, entry.name);
3673
+ await fs$1.copyFile(srcPath, destPath);
3674
+ console.log(` -> ${destPath}`);
3675
+ }
3676
+ }
3677
+ function runNpmInstall(cwd) {
3678
+ return new Promise((resolve, reject) => {
3679
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
3680
+ const child = child_process.spawn(npm, ["install"], { cwd, stdio: "inherit", shell: true });
3681
+ child.on("close", (code) => {
3682
+ if (code !== 0) {
3683
+ reject(new Error(`npm install exited with code ${code}`));
3684
+ return;
3685
+ }
3686
+ resolve();
3687
+ });
3688
+ child.on("error", reject);
3689
+ });
3690
+ }
3691
+ const main$2 = async () => {
3692
+ 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)))) {
3693
+ return;
3694
+ }
3695
+ const { values } = getArgs();
3696
+ if (!values.docker) {
3697
+ return;
3698
+ }
3699
+ const projectName = values.output || "backtest-kit-docker";
3700
+ const projectPath = path.join(process.cwd(), projectName);
3701
+ const templatePath = path.join(__dirname$1, "../docker");
3702
+ const isEmpty = await isDirEmpty(projectPath);
3703
+ if (!isEmpty) {
3704
+ console.error(`Directory "${projectName}" already exists and is not empty.`);
3705
+ process.exit(1);
3706
+ }
3707
+ console.log(`Creating Docker workspace in ${projectPath}`);
3708
+ await copyDir(templatePath, projectPath);
3709
+ console.log(`Installing dependencies...`);
3710
+ await runNpmInstall(projectPath);
3711
+ console.log(`Done! Docker workspace created at ${projectPath}`);
3712
+ console.log(`Next steps:`);
3713
+ console.log(` cd ${projectName}`);
3714
+ console.log(` docker compose up -d`);
3715
+ console.log(` docker compose logs -f`);
3716
+ process.exit(0);
3717
+ };
3640
3718
  main$2();
3641
3719
 
3642
3720
  const ENTRY_PATH = "./node_modules/@backtest-kit/cli/build/index.mjs";
@@ -3657,6 +3735,7 @@ Modes:
3657
3735
  --brokerdebug Fire a single broker commit against the live broker adapter
3658
3736
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
3659
3737
  --init Scaffold a new project in the current directory
3738
+ --docker Scaffold a Docker workspace for running strategies in a container
3660
3739
  --help Print this help message
3661
3740
 
3662
3741
  Backtest flags:
@@ -3766,6 +3845,14 @@ Init flags (--init):
3766
3845
 
3767
3846
  Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
3768
3847
 
3848
+ Docker flags (--docker):
3849
+
3850
+ --output <string> Target directory name (default: backtest-kit-docker)
3851
+
3852
+ Scaffolds a Docker workspace: docker-compose.yaml, .env.example, package.json,
3853
+ tsconfig.json, and a sample strategy under content/. Run npm install then
3854
+ docker compose up to start the container.
3855
+
3769
3856
  Module hooks (loaded automatically by each mode):
3770
3857
 
3771
3858
  modules/backtest.module --backtest Broker adapter for backtest
@@ -3807,6 +3894,7 @@ Examples:
3807
3894
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
3808
3895
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
3809
3896
  node ${ENTRY_PATH} --init --output my-trading-bot
3897
+ node ${ENTRY_PATH} --docker --output my-docker-workspace
3810
3898
  `.trimStart();
3811
3899
  const main$1 = async () => {
3812
3900
  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)))) {
@@ -3816,7 +3904,7 @@ const main$1 = async () => {
3816
3904
  if (!values.help) {
3817
3905
  return;
3818
3906
  }
3819
- process.stdout.write(`@backtest-kit/cli ${"8.2.0"}\n\n`);
3907
+ process.stdout.write(`@backtest-kit/cli ${"8.3.0"}\n\n`);
3820
3908
  process.stdout.write(HELP_TEXT);
3821
3909
  process.exit(0);
3822
3910
  };
@@ -3830,7 +3918,7 @@ const main = async () => {
3830
3918
  if (!values.version) {
3831
3919
  return;
3832
3920
  }
3833
- process.stdout.write(`@backtest-kit/cli ${"8.2.0"}\n`);
3921
+ process.stdout.write(`@backtest-kit/cli ${"8.3.0"}\n`);
3834
3922
  process.exit(0);
3835
3923
  };
3836
3924
  main();
package/build/index.mjs CHANGED
@@ -241,16 +241,16 @@ const TYPES = {
241
241
 
242
242
  const entrySubject = new BehaviorSubject();
243
243
 
244
- const __filename$1 = fileURLToPath(import.meta.url);
245
- const __dirname$1 = path.dirname(__filename$1);
244
+ const __filename$2 = fileURLToPath(import.meta.url);
245
+ const __dirname$2 = path.dirname(__filename$2);
246
246
  let _is_launched = false;
247
247
  class ResolveService {
248
248
  constructor() {
249
249
  this.loggerService = inject(TYPES.loggerService);
250
250
  this.loaderService = inject(TYPES.loaderService);
251
- this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname$1, '..', 'template');
252
- this.DEFAULT_MODULES_DIR = path.resolve(__dirname$1, '..', 'modules');
253
- this.DEFAULT_CONFIG_DIR = path.resolve(__dirname$1, '..', 'config');
251
+ this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname$2, '..', 'template');
252
+ this.DEFAULT_MODULES_DIR = path.resolve(__dirname$2, '..', 'modules');
253
+ this.DEFAULT_CONFIG_DIR = path.resolve(__dirname$2, '..', 'config');
254
254
  this.OVERRIDE_TEMPLATE_DIR = path.resolve(process.cwd(), 'template');
255
255
  this.OVERRIDE_MODULES_DIR = path.resolve(process.cwd(), 'modules');
256
256
  this.OVERRIDE_CONFIG_DIR = path.resolve(process.cwd(), 'config');
@@ -573,6 +573,10 @@ const getArgs = singleshot(() => {
573
573
  type: "boolean",
574
574
  default: false,
575
575
  },
576
+ docker: {
577
+ type: "boolean",
578
+ default: false,
579
+ },
576
580
  help: {
577
581
  type: "boolean",
578
582
  default: false,
@@ -2978,14 +2982,14 @@ const cli = {
2978
2982
  };
2979
2983
  init();
2980
2984
 
2981
- const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "brokerdebug", "flush", "init", "help", "version"];
2985
+ const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "brokerdebug", "flush", "init", "docker", "help", "version"];
2982
2986
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
2983
2987
  const HELP_TEXT$1 = `
2984
2988
  Example:
2985
2989
 
2986
2990
  node ${ENTRY_PATH$1} --help
2987
2991
  `.trimStart();
2988
- const main$e = async () => {
2992
+ const main$f = async () => {
2989
2993
  if (!getEntry(import.meta.url)) {
2990
2994
  return;
2991
2995
  }
@@ -2993,14 +2997,14 @@ const main$e = async () => {
2993
2997
  if (MODES.some((mode) => values[mode])) {
2994
2998
  return;
2995
2999
  }
2996
- process.stdout.write(`@backtest-kit/cli ${"8.2.0"}\n`);
3000
+ process.stdout.write(`@backtest-kit/cli ${"8.3.0"}\n`);
2997
3001
  process.stdout.write("\n");
2998
3002
  process.stdout.write(`Run with --help to see available commands.\n`);
2999
3003
  process.stdout.write("\n");
3000
3004
  process.stdout.write(HELP_TEXT$1);
3001
3005
  process.exit(0);
3002
3006
  };
3003
- main$e();
3007
+ main$f();
3004
3008
 
3005
3009
  const notifyShutdown = singleshot(async () => {
3006
3010
  console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
@@ -3016,7 +3020,7 @@ const flush = async (entryPoint) => {
3016
3020
  console.log(`Removed: ${target}`);
3017
3021
  }
3018
3022
  };
3019
- const main$d = async () => {
3023
+ const main$e = async () => {
3020
3024
  if (!getEntry(import.meta.url)) {
3021
3025
  return;
3022
3026
  }
@@ -3033,7 +3037,7 @@ const main$d = async () => {
3033
3037
  }
3034
3038
  process.exit(0);
3035
3039
  };
3036
- main$d();
3040
+ main$e();
3037
3041
 
3038
3042
  const BEFORE_EXIT_FN$5 = singleshot(async () => {
3039
3043
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3055,7 +3059,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
3055
3059
  const listenGracefulShutdown$5 = singleshot(() => {
3056
3060
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3057
3061
  });
3058
- const main$c = async () => {
3062
+ const main$d = async () => {
3059
3063
  if (!getEntry(import.meta.url)) {
3060
3064
  return;
3061
3065
  }
@@ -3070,7 +3074,7 @@ const main$c = async () => {
3070
3074
  await cli.backtestMainService.connect();
3071
3075
  listenGracefulShutdown$5();
3072
3076
  };
3073
- main$c();
3077
+ main$d();
3074
3078
 
3075
3079
  const BEFORE_EXIT_FN$4 = singleshot(async () => {
3076
3080
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3088,7 +3092,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
3088
3092
  const listenGracefulShutdown$4 = singleshot(() => {
3089
3093
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3090
3094
  });
3091
- const main$b = async () => {
3095
+ const main$c = async () => {
3092
3096
  if (!getEntry(import.meta.url)) {
3093
3097
  return;
3094
3098
  }
@@ -3104,7 +3108,7 @@ const main$b = async () => {
3104
3108
  listenGracefulShutdown$4();
3105
3109
  await cli.walkerMainService.connect();
3106
3110
  };
3107
- main$b();
3111
+ main$c();
3108
3112
 
3109
3113
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
3110
3114
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3125,7 +3129,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
3125
3129
  const listenGracefulShutdown$3 = singleshot(() => {
3126
3130
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3127
3131
  });
3128
- const main$a = async () => {
3132
+ const main$b = async () => {
3129
3133
  if (!getEntry(import.meta.url)) {
3130
3134
  return;
3131
3135
  }
@@ -3136,7 +3140,7 @@ const main$a = async () => {
3136
3140
  cli.paperMainService.connect();
3137
3141
  listenGracefulShutdown$3();
3138
3142
  };
3139
- main$a();
3143
+ main$b();
3140
3144
 
3141
3145
  const BEFORE_EXIT_FN$2 = singleshot(async () => {
3142
3146
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -3157,7 +3161,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
3157
3161
  const listenGracefulShutdown$2 = singleshot(() => {
3158
3162
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3159
3163
  });
3160
- const main$9 = async () => {
3164
+ const main$a = async () => {
3161
3165
  if (!getEntry(import.meta.url)) {
3162
3166
  return;
3163
3167
  }
@@ -3168,7 +3172,7 @@ const main$9 = async () => {
3168
3172
  await cli.liveMainService.connect();
3169
3173
  listenGracefulShutdown$2();
3170
3174
  };
3171
- main$9();
3175
+ main$a();
3172
3176
 
3173
3177
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
3174
3178
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -3178,7 +3182,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
3178
3182
  const listenGracefulShutdown$1 = singleshot(() => {
3179
3183
  process.on("SIGINT", BEFORE_EXIT_FN$1);
3180
3184
  });
3181
- const main$8 = async () => {
3185
+ const main$9 = async () => {
3182
3186
  if (!getEntry(import.meta.url)) {
3183
3187
  return;
3184
3188
  }
@@ -3188,7 +3192,7 @@ const main$8 = async () => {
3188
3192
  }
3189
3193
  listenGracefulShutdown$1();
3190
3194
  };
3191
- main$8();
3195
+ main$9();
3192
3196
 
3193
3197
  const BEFORE_EXIT_FN = singleshot(async () => {
3194
3198
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -3198,7 +3202,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
3198
3202
  const listenGracefulShutdown = singleshot(() => {
3199
3203
  process.on("SIGINT", BEFORE_EXIT_FN);
3200
3204
  });
3201
- const main$7 = async () => {
3205
+ const main$8 = async () => {
3202
3206
  if (!getEntry(import.meta.url)) {
3203
3207
  return;
3204
3208
  }
@@ -3208,7 +3212,7 @@ const main$7 = async () => {
3208
3212
  }
3209
3213
  listenGracefulShutdown();
3210
3214
  };
3211
- main$7();
3215
+ main$8();
3212
3216
 
3213
3217
  const EXTRACT_ROWS_FN = (plots, schema) => {
3214
3218
  const keys = Object.keys(schema);
@@ -3230,7 +3234,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
3230
3234
  }
3231
3235
  return rows;
3232
3236
  };
3233
- const main$6 = async () => {
3237
+ const main$7 = async () => {
3234
3238
  if (!getEntry(import.meta.url)) {
3235
3239
  return;
3236
3240
  }
@@ -3310,9 +3314,9 @@ const main$6 = async () => {
3310
3314
  console.log(await toMarkdown(signalId, plots, signalSchema));
3311
3315
  process.exit(0);
3312
3316
  };
3313
- main$6();
3317
+ main$7();
3314
3318
 
3315
- const main$5 = async () => {
3319
+ const main$6 = async () => {
3316
3320
  if (!getEntry(import.meta.url)) {
3317
3321
  return;
3318
3322
  }
@@ -3347,9 +3351,9 @@ const main$5 = async () => {
3347
3351
  };
3348
3352
  process.on("SIGINT", beforeExit);
3349
3353
  };
3350
- main$5();
3354
+ main$6();
3351
3355
 
3352
- const main$4 = async () => {
3356
+ const main$5 = async () => {
3353
3357
  if (!getEntry(import.meta.url)) {
3354
3358
  return;
3355
3359
  }
@@ -3410,9 +3414,9 @@ const main$4 = async () => {
3410
3414
  console.log(JSON.stringify(candles, null, 2));
3411
3415
  process.exit(0);
3412
3416
  };
3413
- main$4();
3417
+ main$5();
3414
3418
 
3415
- const main$3 = async () => {
3419
+ const main$4 = async () => {
3416
3420
  if (!getEntry(import.meta.url)) {
3417
3421
  return;
3418
3422
  }
@@ -3506,16 +3510,16 @@ const main$3 = async () => {
3506
3510
  }
3507
3511
  process.exit(0);
3508
3512
  };
3509
- main$3();
3513
+ main$4();
3510
3514
 
3511
- const __filename = fileURLToPath(import.meta.url);
3512
- const __dirname = dirname(__filename);
3515
+ const __filename$1 = fileURLToPath(import.meta.url);
3516
+ const __dirname$1 = dirname(__filename$1);
3513
3517
  const MUSTACHE_EXT = ".mustache";
3514
3518
  const MUSTACHE_RENAME = {
3515
3519
  "gitignore": ".gitignore",
3516
3520
  "package": "package.json",
3517
3521
  };
3518
- async function isDirEmpty(dirPath) {
3522
+ async function isDirEmpty$1(dirPath) {
3519
3523
  try {
3520
3524
  const files = await readdir(dirPath);
3521
3525
  return files.length === 0;
@@ -3527,13 +3531,13 @@ async function isDirEmpty(dirPath) {
3527
3531
  throw error;
3528
3532
  }
3529
3533
  }
3530
- async function copyDir(srcDir, destDir, data) {
3534
+ async function copyDir$1(srcDir, destDir, data) {
3531
3535
  await mkdir(destDir, { recursive: true });
3532
3536
  const entries = await readdir(srcDir, { withFileTypes: true });
3533
3537
  for (const entry of entries) {
3534
3538
  const srcPath = join(srcDir, entry.name);
3535
3539
  if (entry.isDirectory()) {
3536
- await copyDir(srcPath, join(destDir, entry.name), data);
3540
+ await copyDir$1(srcPath, join(destDir, entry.name), data);
3537
3541
  continue;
3538
3542
  }
3539
3543
  if (entry.name === ".gitkeep") {
@@ -3569,7 +3573,7 @@ function runScript(scriptPath, cwd) {
3569
3573
  child.on("error", reject);
3570
3574
  });
3571
3575
  }
3572
- function runNpmInstall(cwd) {
3576
+ function runNpmInstall$1(cwd) {
3573
3577
  return new Promise((resolve, reject) => {
3574
3578
  const npm = process.platform === "win32" ? "npm.cmd" : "npm";
3575
3579
  const child = spawn(npm, ["install"], { cwd, stdio: "inherit", shell: true });
@@ -3583,7 +3587,7 @@ function runNpmInstall(cwd) {
3583
3587
  child.on("error", reject);
3584
3588
  });
3585
3589
  }
3586
- const main$2 = async () => {
3590
+ const main$3 = async () => {
3587
3591
  if (!getEntry(import.meta.url)) {
3588
3592
  return;
3589
3593
  }
@@ -3593,21 +3597,95 @@ const main$2 = async () => {
3593
3597
  }
3594
3598
  const projectName = values.output || "backtest-kit-project";
3595
3599
  const projectPath = join(process.cwd(), projectName);
3596
- const templatePath = join(__dirname, "../template/project");
3597
- const isEmpty = await isDirEmpty(projectPath);
3600
+ const templatePath = join(__dirname$1, "../template/project");
3601
+ const isEmpty = await isDirEmpty$1(projectPath);
3598
3602
  if (!isEmpty) {
3599
3603
  console.error(`Directory "${projectName}" already exists and is not empty.`);
3600
3604
  process.exit(1);
3601
3605
  }
3602
3606
  console.log(`Creating project in ${projectPath}`);
3603
- await copyDir(templatePath, projectPath, { PROJECT_NAME: projectName });
3607
+ await copyDir$1(templatePath, projectPath, { PROJECT_NAME: projectName });
3604
3608
  console.log(`Fetching docs...`);
3605
3609
  await runScript(join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
3606
3610
  console.log(`Installing dependencies...`);
3607
- await runNpmInstall(projectPath);
3611
+ await runNpmInstall$1(projectPath);
3608
3612
  console.log(`Done! Project created at ${projectPath}`);
3609
3613
  process.exit(0);
3610
3614
  };
3615
+ main$3();
3616
+
3617
+ const __filename = fileURLToPath(import.meta.url);
3618
+ const __dirname = dirname(__filename);
3619
+ async function isDirEmpty(dirPath) {
3620
+ try {
3621
+ const files = await readdir(dirPath);
3622
+ return files.length === 0;
3623
+ }
3624
+ catch (error) {
3625
+ if (error.code === "ENOENT") {
3626
+ return true;
3627
+ }
3628
+ throw error;
3629
+ }
3630
+ }
3631
+ async function copyDir(srcDir, destDir) {
3632
+ await mkdir(destDir, { recursive: true });
3633
+ const entries = await readdir(srcDir, { withFileTypes: true });
3634
+ for (const entry of entries) {
3635
+ const srcPath = join(srcDir, entry.name);
3636
+ if (entry.isDirectory()) {
3637
+ await copyDir(srcPath, join(destDir, entry.name));
3638
+ continue;
3639
+ }
3640
+ if (entry.name === ".gitkeep") {
3641
+ continue;
3642
+ }
3643
+ const destPath = join(destDir, entry.name);
3644
+ await copyFile(srcPath, destPath);
3645
+ console.log(` -> ${destPath}`);
3646
+ }
3647
+ }
3648
+ function runNpmInstall(cwd) {
3649
+ return new Promise((resolve, reject) => {
3650
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
3651
+ const child = spawn(npm, ["install"], { cwd, stdio: "inherit", shell: true });
3652
+ child.on("close", (code) => {
3653
+ if (code !== 0) {
3654
+ reject(new Error(`npm install exited with code ${code}`));
3655
+ return;
3656
+ }
3657
+ resolve();
3658
+ });
3659
+ child.on("error", reject);
3660
+ });
3661
+ }
3662
+ const main$2 = async () => {
3663
+ if (!getEntry(import.meta.url)) {
3664
+ return;
3665
+ }
3666
+ const { values } = getArgs();
3667
+ if (!values.docker) {
3668
+ return;
3669
+ }
3670
+ const projectName = values.output || "backtest-kit-docker";
3671
+ const projectPath = join(process.cwd(), projectName);
3672
+ const templatePath = join(__dirname, "../docker");
3673
+ const isEmpty = await isDirEmpty(projectPath);
3674
+ if (!isEmpty) {
3675
+ console.error(`Directory "${projectName}" already exists and is not empty.`);
3676
+ process.exit(1);
3677
+ }
3678
+ console.log(`Creating Docker workspace in ${projectPath}`);
3679
+ await copyDir(templatePath, projectPath);
3680
+ console.log(`Installing dependencies...`);
3681
+ await runNpmInstall(projectPath);
3682
+ console.log(`Done! Docker workspace created at ${projectPath}`);
3683
+ console.log(`Next steps:`);
3684
+ console.log(` cd ${projectName}`);
3685
+ console.log(` docker compose up -d`);
3686
+ console.log(` docker compose logs -f`);
3687
+ process.exit(0);
3688
+ };
3611
3689
  main$2();
3612
3690
 
3613
3691
  const ENTRY_PATH = "./node_modules/@backtest-kit/cli/build/index.mjs";
@@ -3628,6 +3706,7 @@ Modes:
3628
3706
  --brokerdebug Fire a single broker commit against the live broker adapter
3629
3707
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
3630
3708
  --init Scaffold a new project in the current directory
3709
+ --docker Scaffold a Docker workspace for running strategies in a container
3631
3710
  --help Print this help message
3632
3711
 
3633
3712
  Backtest flags:
@@ -3737,6 +3816,14 @@ Init flags (--init):
3737
3816
 
3738
3817
  Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
3739
3818
 
3819
+ Docker flags (--docker):
3820
+
3821
+ --output <string> Target directory name (default: backtest-kit-docker)
3822
+
3823
+ Scaffolds a Docker workspace: docker-compose.yaml, .env.example, package.json,
3824
+ tsconfig.json, and a sample strategy under content/. Run npm install then
3825
+ docker compose up to start the container.
3826
+
3740
3827
  Module hooks (loaded automatically by each mode):
3741
3828
 
3742
3829
  modules/backtest.module --backtest Broker adapter for backtest
@@ -3778,6 +3865,7 @@ Examples:
3778
3865
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
3779
3866
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
3780
3867
  node ${ENTRY_PATH} --init --output my-trading-bot
3868
+ node ${ENTRY_PATH} --docker --output my-docker-workspace
3781
3869
  `.trimStart();
3782
3870
  const main$1 = async () => {
3783
3871
  if (!getEntry(import.meta.url)) {
@@ -3787,7 +3875,7 @@ const main$1 = async () => {
3787
3875
  if (!values.help) {
3788
3876
  return;
3789
3877
  }
3790
- process.stdout.write(`@backtest-kit/cli ${"8.2.0"}\n\n`);
3878
+ process.stdout.write(`@backtest-kit/cli ${"8.3.0"}\n\n`);
3791
3879
  process.stdout.write(HELP_TEXT);
3792
3880
  process.exit(0);
3793
3881
  };
@@ -3801,7 +3889,7 @@ const main = async () => {
3801
3889
  if (!values.version) {
3802
3890
  return;
3803
3891
  }
3804
- process.stdout.write(`@backtest-kit/cli ${"8.2.0"}\n`);
3892
+ process.stdout.write(`@backtest-kit/cli ${"8.3.0"}\n`);
3805
3893
  process.exit(0);
3806
3894
  };
3807
3895
  main();
@@ -0,0 +1,2 @@
1
+ CC_REDIS_HOST=host.docker.internal
2
+ CC_MONGO_CONNECTION_STRING=mongodb://host.docker.internal:27017/backtest-kit?wtimeoutMS=15000
@@ -0,0 +1,11 @@
1
+ import {
2
+ addStrategySchema,
3
+ } from "backtest-kit";
4
+
5
+ addStrategySchema({
6
+ strategyName: "feb_2026_strategy",
7
+ getSignal: async (_symbol, when) => {
8
+ console.log(when);
9
+ return null;
10
+ },
11
+ });
@@ -0,0 +1,83 @@
1
+ import { addExchangeSchema, addFrameSchema, roundTicks } from "backtest-kit";
2
+ import { singleshot } from "functools-kit";
3
+ import ccxt from "ccxt";
4
+
5
+ const getExchange = singleshot(async () => {
6
+ const exchange = new ccxt.binance({
7
+ options: {
8
+ defaultType: "spot",
9
+ adjustForTimeDifference: true,
10
+ recvWindow: 60000,
11
+ },
12
+ enableRateLimit: true,
13
+ });
14
+ await exchange.loadMarkets();
15
+ return exchange;
16
+ });
17
+
18
+ addExchangeSchema({
19
+ exchangeName: "ccxt-exchange",
20
+ getCandles: async (symbol, interval, since, limit) => {
21
+ const exchange = await getExchange();
22
+ const candles = await exchange.fetchOHLCV(
23
+ symbol,
24
+ interval,
25
+ since.getTime(),
26
+ limit,
27
+ );
28
+ return candles.map(([timestamp, open, high, low, close, volume]) => ({
29
+ timestamp,
30
+ open,
31
+ high,
32
+ low,
33
+ close,
34
+ volume,
35
+ }));
36
+ },
37
+ getOrderBook: async (symbol, depth, _from, _to, backtest) => {
38
+ if (backtest) {
39
+ throw new Error(
40
+ "Order book fetching is not supported in backtest mode for the default exchange schema. Please implement it according to your needs.",
41
+ );
42
+ }
43
+ const exchange = await getExchange();
44
+ const bookData = await exchange.fetchOrderBook(symbol, depth);
45
+ return {
46
+ symbol,
47
+ asks: bookData.asks.map(([price, quantity]) => ({
48
+ price: String(price),
49
+ quantity: String(quantity),
50
+ })),
51
+ bids: bookData.bids.map(([price, quantity]) => ({
52
+ price: String(price),
53
+ quantity: String(quantity),
54
+ })),
55
+ };
56
+ },
57
+ formatPrice: async (symbol, price) => {
58
+ const exchange = await getExchange();
59
+ const market = exchange.market(symbol);
60
+ const tickSize = market.limits?.price?.min || market.precision?.price;
61
+ if (tickSize !== undefined) {
62
+ return roundTicks(price, tickSize);
63
+ }
64
+ return exchange.priceToPrecision(symbol, price);
65
+ },
66
+ formatQuantity: async (symbol, quantity) => {
67
+ const exchange = await getExchange();
68
+ const market = exchange.market(symbol);
69
+ const stepSize = market.limits?.amount?.min || market.precision?.amount;
70
+ if (stepSize !== undefined) {
71
+ return roundTicks(quantity, stepSize);
72
+ }
73
+ return exchange.amountToPrecision(symbol, quantity);
74
+ },
75
+ });
76
+
77
+ addFrameSchema({
78
+ frameName: "feb_2026_frame",
79
+ interval: "1m",
80
+ startDate: new Date("2026-02-01T00:00:00Z"),
81
+ endDate: new Date("2026-02-28T23:59:59Z"),
82
+ note: "February 2026",
83
+ });
@@ -0,0 +1,24 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ backtest:
5
+ image: tripolskypetr/backtest-kit
6
+ network_mode: host
7
+ extra_hosts:
8
+ - "host.docker.internal:host-gateway"
9
+ container_name: backtest
10
+ ports:
11
+ - "60050:60050"
12
+ restart: unless-stopped
13
+ volumes:
14
+ - ./:/workspace
15
+ working_dir: /workspace
16
+ command:
17
+ - --backtest
18
+ - --symbol
19
+ - BTCUSDT
20
+ - --strategy
21
+ - feb_2026_strategy
22
+ - --exchange
23
+ - ccxt-exchange
24
+ - ./content/feb_2026/feb_2026.strategy.ts
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "example",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node ./node_modules/@backtest-kit/cli/build/index.mjs",
8
+ "start:debug": "node --inspect-brk ./node_modules/@backtest-kit/cli/build/index.mjs"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "ISC",
13
+ "type": "commonjs",
14
+ "devDependencies": {
15
+ "@types/node": "25.6.0"
16
+ },
17
+ "dependencies": {
18
+ "@backtest-kit/cli": "8.3.0",
19
+ "@backtest-kit/graph": "8.3.0",
20
+ "@backtest-kit/pinets": "8.3.0",
21
+ "@backtest-kit/signals": "8.3.0",
22
+ "@backtest-kit/ui": "8.3.0",
23
+ "@tavily/core": "0.7.2",
24
+ "@tensorflow/tfjs": "4.22.0",
25
+ "@tensorflow/tfjs-backend-wasm": "4.22.0",
26
+ "@tensorflow/tfjs-core": "4.22.0",
27
+ "agent-swarm-kit": "2.6.0",
28
+ "backtest-kit": "8.3.0",
29
+ "dayjs": "1.11.20",
30
+ "functools-kit": "2.3.0",
31
+ "garch": "1.2.3",
32
+ "get-moment-stamp": "1.1.2",
33
+ "jsonrepair": "3.12.0",
34
+ "ollama": "0.6.3",
35
+ "slugify": "1.6.9",
36
+ "volume-anomaly": "1.2.3"
37
+ }
38
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "compilerOptions": {
3
+ "ignoreDeprecations": "6.0",
4
+ "lib": [
5
+ "esnext",
6
+ "dom"
7
+ ],
8
+ "types": [
9
+ "node"
10
+ ],
11
+ "moduleDetection": "force",
12
+ "target": "ES2020",
13
+ "module": "ESNext",
14
+ "moduleResolution": "bundler",
15
+ "noEmit": true,
16
+ "strict": false,
17
+ "downlevelIteration": true,
18
+ "noImplicitAny": false,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUnusedLocals": false,
22
+ "noUnusedParameters": false,
23
+ "noPropertyAccessFromIndexSignature": false,
24
+ "paths": {
25
+ "logic": ["./logic/index.ts"],
26
+ "logic/*": ["./logic/*"],
27
+ "utils": ["./utils/index.ts"],
28
+ "utils/*": ["./utils/*"]
29
+ }
30
+ },
31
+ "include": [
32
+ "./logic",
33
+ "./content",
34
+ "./modules",
35
+ ],
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/cli",
3
- "version": "8.2.0",
3
+ "version": "8.3.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",
@@ -35,6 +35,7 @@
35
35
  "types.d.ts",
36
36
  "template",
37
37
  "config",
38
+ "docker",
38
39
  "README.md"
39
40
  ],
40
41
  "repository": {
@@ -62,11 +63,11 @@
62
63
  "devDependencies": {
63
64
  "@babel/plugin-transform-modules-umd": "7.27.1",
64
65
  "@babel/standalone": "7.29.1",
65
- "@backtest-kit/graph": "8.2.0",
66
- "@backtest-kit/ollama": "8.2.0",
67
- "@backtest-kit/pinets": "8.2.0",
68
- "@backtest-kit/signals": "8.2.0",
69
- "@backtest-kit/ui": "8.2.0",
66
+ "@backtest-kit/graph": "8.3.0",
67
+ "@backtest-kit/ollama": "8.3.0",
68
+ "@backtest-kit/pinets": "8.3.0",
69
+ "@backtest-kit/signals": "8.3.0",
70
+ "@backtest-kit/ui": "8.3.0",
70
71
  "@rollup/plugin-replace": "6.0.3",
71
72
  "@rollup/plugin-typescript": "11.1.6",
72
73
  "@types/image-size": "0.7.0",
@@ -74,7 +75,7 @@
74
75
  "@types/mustache": "4.2.6",
75
76
  "@types/node": "22.9.0",
76
77
  "@types/stack-trace": "0.0.33",
77
- "backtest-kit": "8.2.0",
78
+ "backtest-kit": "8.3.0",
78
79
  "glob": "11.0.1",
79
80
  "markdown-it": "14.1.1",
80
81
  "rimraf": "6.0.1",
@@ -89,12 +90,12 @@
89
90
  "peerDependencies": {
90
91
  "@babel/plugin-transform-modules-umd": "^7.27.1",
91
92
  "@babel/standalone": "^7.29.1",
92
- "@backtest-kit/graph": "^8.2.0",
93
- "@backtest-kit/ollama": "^8.2.0",
94
- "@backtest-kit/pinets": "^8.2.0",
95
- "@backtest-kit/signals": "^8.2.0",
96
- "@backtest-kit/ui": "^8.2.0",
97
- "backtest-kit": "^8.2.0",
93
+ "@backtest-kit/graph": "^8.3.0",
94
+ "@backtest-kit/ollama": "^8.3.0",
95
+ "@backtest-kit/pinets": "^8.3.0",
96
+ "@backtest-kit/signals": "^8.3.0",
97
+ "@backtest-kit/ui": "^8.3.0",
98
+ "backtest-kit": "^8.3.0",
98
99
  "markdown-it": "^14.1.1",
99
100
  "typescript": "^5.0.0"
100
101
  },
@@ -117,7 +118,7 @@
117
118
  "telegraf": "4.15.3"
118
119
  },
119
120
  "bin": {
120
- "@backtest-kit/cli": "./build/index.mjs"
121
+ "backtest-kit": "./build/index.mjs"
121
122
  },
122
123
  "publishConfig": {
123
124
  "access": "public"
@@ -13,12 +13,12 @@
13
13
  "license": "ISC",
14
14
  "type": "commonjs",
15
15
  "dependencies": {
16
- "@backtest-kit/cli": "^8.2.0",
17
- "@backtest-kit/graph": "^8.2.0",
18
- "@backtest-kit/pinets": "^8.2.0",
19
- "@backtest-kit/ui": "^8.2.0",
16
+ "@backtest-kit/cli": "^8.3.0",
17
+ "@backtest-kit/graph": "^8.3.0",
18
+ "@backtest-kit/pinets": "^8.3.0",
19
+ "@backtest-kit/ui": "^8.3.0",
20
20
  "agent-swarm-kit": "^2.6.0",
21
- "backtest-kit": "^8.2.0",
21
+ "backtest-kit": "^8.3.0",
22
22
  "functools-kit": "^2.3.0",
23
23
  "garch": "^1.2.3",
24
24
  "get-moment-stamp": "^1.1.2",